Load and Explore Data

strokes <- read.csv("./data/stroke_data.csv")
str(strokes)
'data.frame':   5110 obs. of  12 variables:
 $ id               : int  9046 51676 31112 60182 1665 56669 53882 10434 27419 60491 ...
 $ gender           : chr  "Male" "Female" "Male" "Female" ...
 $ age              : num  67 61 80 49 79 81 74 69 59 78 ...
 $ hypertension     : int  0 0 0 0 1 0 1 0 0 0 ...
 $ heart_disease    : int  1 0 1 0 0 0 1 0 0 0 ...
 $ ever_married     : chr  "Yes" "Yes" "Yes" "Yes" ...
 $ work_type        : chr  "Private" "Self-employed" "Private" "Private" ...
 $ Residence_type   : chr  "Urban" "Rural" "Rural" "Urban" ...
 $ avg_glucose_level: num  229 202 106 171 174 ...
 $ bmi              : chr  "36.6" "N/A" "32.5" "34.4" ...
 $ smoking_status   : chr  "formerly smoked" "never smoked" "never smoked" "smokes" ...
 $ stroke           : int  1 1 1 1 1 1 1 1 1 1 ...
summary(strokes)
       id           gender               age         hypertension     heart_disease    
 Min.   :   67   Length:5110        Min.   : 0.08   Min.   :0.00000   Min.   :0.00000  
 1st Qu.:17741   Class :character   1st Qu.:25.00   1st Qu.:0.00000   1st Qu.:0.00000  
 Median :36932   Mode  :character   Median :45.00   Median :0.00000   Median :0.00000  
 Mean   :36518                      Mean   :43.23   Mean   :0.09746   Mean   :0.05401  
 3rd Qu.:54682                      3rd Qu.:61.00   3rd Qu.:0.00000   3rd Qu.:0.00000  
 Max.   :72940                      Max.   :82.00   Max.   :1.00000   Max.   :1.00000  
 ever_married        work_type         Residence_type     avg_glucose_level     bmi           
 Length:5110        Length:5110        Length:5110        Min.   : 55.12    Length:5110       
 Class :character   Class :character   Class :character   1st Qu.: 77.25    Class :character  
 Mode  :character   Mode  :character   Mode  :character   Median : 91.89    Mode  :character  
                                                          Mean   :106.15                      
                                                          3rd Qu.:114.09                      
                                                          Max.   :271.74                      
 smoking_status         stroke       
 Length:5110        Min.   :0.00000  
 Class :character   1st Qu.:0.00000  
 Mode  :character   Median :0.00000  
                    Mean   :0.04873  
                    3rd Qu.:0.00000  
                    Max.   :1.00000  

Verify that the ID column is in fact unique

Remove ID column since this is an assigned identifier and as such does not impact stroke

uniqueID <- unique(strokes$id)
length(uniqueID)
[1] 5110
dim(strokes)
[1] 5110   12
strokes <- strokes[-1]

Since all the positive stroke values are found at the start of the file we will randomize the dataframe

set.seed(42)
rows <- sample(nrow(strokes))
strokes <- strokes[rows,]

Review category distrubution for categorical variables

Gender Entries
Female Male Other
2994 2115 1
Ever Married Entries
No Yes
1757 3353
Work Type Entries
children Govt_job Never_worked Private Self-employed
687 657 22 2925 819
Residence TypeEntries
Rural Urban
2514 2596
Smoking Status Entries
formerly smoked never smoked smokes Unknown
885 1892 789 1544
HypertensionEntries
0 1
4612 498
Heart Disease Entries
0 1
4834 276
Smoking Status Entries
formerly smoked never smoked smokes Unknown
885 1892 789 1544
Stroke Entries
0 1
4861 249

Update binary categorical variables to numeric

remove gender with value of ‘other’, code "Female as 0, male as 1

strokes <- strokes %>% filter(gender != "Other")
strokes$gender <- ifelse(strokes$gender == "Female", 0,1)

convert residence type to binary response. 0=rural, 1=urban

strokes$Residence_type = ifelse(strokes$Residence_type == "Rural", 0, 1)

convert ever married to binary response. 0=No, 1=Yes

strokes$ever_married <- ifelse(strokes$ever_married == 'No', 0,1)

Convert stroke to ‘yes/no’ values

strokes$stroke <- ifelse(strokes$stroke == 1, "Yes", "No")

Convert multi-category categorical features to factors

# strokes$gender <- as.factor(strokes$gender)
# strokes$hypertension <- as.factor(strokes$hypertension)
# strokes$heart_disease <- as.factor(strokes$heart_disease)
# strokes$ever_married <- as.factor(strokes$ever_married)
strokes$work_type <- as.factor(strokes$work_type)
# strokes$Residence_type <- as.factor(strokes$Residence_type)
strokes$smoking_status <- as.factor(strokes$smoking_status)
strokes$stroke <- as.factor(strokes$stroke)
strokes$smoking_status <- revalue(strokes$smoking_status, c("formerly smoked"="formerly_smoked", "never smoked"="never_smoked", "smokes"="smokes", "Unknown"="unknown"))
strokes$work_type <- revalue(strokes$work_type, c("Self-employed"="Self_employed"))

Clean bmi feature

  • Convert “N/A” entries to NA values
  • Convert from character to numeric values
strokes <- na_if(strokes,"N/A")
strokes$bmi <- as.numeric(strokes$bmi)
dim(strokes)
[1] 5109   11

Data Exploration

Missing Values

Table continues below
gender age hypertension heart_disease ever_married work_type
0 0 0 0 0 0
Residence_type avg_glucose_level bmi smoking_status stroke
0 0 201 0 0
require(tidyverse)
require(rcompanion)


# Calculate a pairwise association between all variables in a data-frame. In particular nominal vs nominal with Chi-square, numeric vs numeric with Pearson correlation, and nominal vs numeric with ANOVA.
# Adopted from https://stackoverflow.com/a/52557631/590437
mixed_assoc = function(df, cor_method="spearman", adjust_cramersv_bias=TRUE){
    df_comb = expand.grid(names(df), names(df),  stringsAsFactors = F) %>% set_names("X1", "X2")

    is_nominal = function(x) class(x) %in% c("factor", "character")
    # https://community.rstudio.com/t/why-is-purr-is-numeric-deprecated/3559
    # https://github.com/r-lib/rlang/issues/781
    is_numeric <- function(x) { is.integer(x) || is_double(x)}

    f = function(xName,yName) {
        x =  pull(df, xName)
        y =  pull(df, yName)

        result = if(is_nominal(x) && is_nominal(y)){
            # use bias corrected cramersV as described in https://rdrr.io/cran/rcompanion/man/cramerV.html
            cv = cramerV(as.character(x), as.character(y), bias.correct = adjust_cramersv_bias)
            data.frame(xName, yName, assoc=cv, type="cramersV")

        }else if(is_numeric(x) && is_numeric(y)){
            correlation = cor(x, y, method=cor_method, use="complete.obs")
            data.frame(xName, yName, assoc=correlation, type="correlation")

        }else if(is_numeric(x) && is_nominal(y)){
            # from https://stats.stackexchange.com/questions/119835/correlation-between-a-nominal-iv-and-a-continuous-dv-variable/124618#124618
            r_squared = summary(lm(x ~ y))$r.squared
            data.frame(xName, yName, assoc=sqrt(r_squared), type="anova")

        }else if(is_nominal(x) && is_numeric(y)){
            r_squared = summary(lm(y ~x))$r.squared
            data.frame(xName, yName, assoc=sqrt(r_squared), type="anova")

        }else {
            warning(paste("unmatched column type combination: ", class(x), class(y)))
        }

        # finally add complete obs number and ratio to table
        result %>% mutate(complete_obs_pairs=sum(!is.na(x) & !is.na(y)), complete_obs_ratio=complete_obs_pairs/length(x)) %>% rename(x=xName, y=yName)
    }

    # apply function to each variable combination
    map2_df(df_comb$X1, df_comb$X2, f)
}

Associations between features

associations <- mixed_assoc(strokes)
associations

bmi imputation

bmi has multiple missing values. Instead of using the mean or median of the entire data set to impute the missing values determine if a second feature has an association with bmi

association of bmi with other variables

bmi_assoc <- associations %>% filter(y == "bmi")

Plots of bmi vs other features

# ggplot(data=associations, aes(x=x, y=y, fill=assoc)) + geom_tile() + theme(axis.text.x=element_text(angle=90, vjust=0.5, hjust=1), axis.title = element_blank(), plot.title=element_text(hjust=0.5)) + ggtitle("Association Between Features")
ggplot(data=bmi_assoc, aes(x=x, y=y, fill=assoc)) + geom_tile() + theme(axis.text.x=element_text(angle=90, vjust=0.5, hjust=1, size=20), axis.text.y = element_text(size=20),axis.title = element_blank(), plot.title=element_text(hjust=0.5)) + ggtitle("Association with bmi")

plot(bmi ~ ., data=strokes)

Age has an association with bmi, so the data will be binned by age and the missing bmi values will be imputed based upon the median value of the bin they fall in

Create bins for age feature and set missing BMI values to median of the bin they fall in

strokes_original <- strokes
strokes$age_cat <- bin(strokes$age, nbins=10, labels=c('a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8','a9', 'a10'), method = "clusters")

strokes <- strokes %>% group_by(age_cat) %>% mutate(bmi.new = median(bmi, na.rm=TRUE))
strokes$bmi <- ifelse(is.na(strokes$bmi), strokes$bmi.new, strokes$bmi)

# remove working columns
strokes <- strokes[-c(12,13)]

Verify missing values are resolved and data types are as expected

Missing Values

Table continues below
gender age hypertension heart_disease ever_married work_type
0 0 0 0 0 0
Residence_type avg_glucose_level bmi smoking_status stroke
0 0 0 0 0



str(strokes)
tibble [5,109 × 11] (S3: tbl_df/tbl/data.frame)
 $ gender           : num [1:5109] 0 0 0 1 1 0 1 1 0 1 ...
 $ age              : num [1:5109] 56 16 28 52 4 42 65 31 66 64 ...
 $ hypertension     : int [1:5109] 0 0 0 0 0 0 0 0 1 0 ...
 $ heart_disease    : int [1:5109] 0 0 0 0 0 0 0 0 0 0 ...
 $ ever_married     : num [1:5109] 1 0 0 1 0 1 1 1 1 1 ...
 $ work_type        : Factor w/ 5 levels "children","Govt_job",..: 4 3 4 4 1 2 4 2 4 4 ...
 $ Residence_type   : num [1:5109] 0 1 0 1 0 1 1 1 0 1 ...
 $ avg_glucose_level: num [1:5109] 77.7 102.1 96.9 229.2 103.8 ...
 $ bmi              : num [1:5109] 40.8 27.1 29 35.6 15.9 29.8 28.4 30.4 39.5 28.3 ...
 $ smoking_status   : Factor w/ 4 levels "formerly_smoked",..: 2 2 4 1 4 4 3 1 2 4 ...
 $ stroke           : Factor w/ 2 levels "No","Yes": 1 1 1 1 1 1 1 1 1 1 ...

Explore relation of features with response variable ‘stroke’

p1 <- ggplot(data=strokes, aes(stroke, fill=as.factor(gender))) + geom_bar(position='fill') + labs(fill="gender")
p2 <- ggplot(data=strokes, aes(stroke, age)) + geom_boxplot()
p3 <- ggplot(data=strokes, aes(stroke, fill=as.factor(hypertension))) + geom_bar(position='fill') + labs(fill="hypertension")
p4 <- ggplot(data=strokes, aes(stroke, fill=as.factor(heart_disease))) + geom_bar(position='fill') + labs(fill='heart_disease')
p5 <- ggplot(data=strokes, aes(stroke, fill=as.factor(ever_married))) + geom_bar(position='fill') + labs(fill="ever_married")
p6 <- ggplot(data=strokes, aes(stroke, fill=work_type)) + geom_bar(position='fill')
p7 <- ggplot(data=strokes, aes(stroke, fill=as.factor(Residence_type))) + geom_bar(position='fill') + labs(fill="Residence_type")
p8 <- ggplot(data=strokes, aes(stroke, avg_glucose_level)) + geom_boxplot()
p9 <- ggplot(data=strokes, aes(stroke, bmi)) + geom_boxplot()
p10 <- ggplot(data=strokes, aes(stroke, fill=smoking_status)) + geom_bar(position='fill')

grid.arrange(p1,p2,p3,p4,nrow=2)

grid.arrange(p5,p6,p7,p8,nrow=2)

grid.arrange(p9,p10,nrow=1)

Chi-squared approximation may be incorrect

Chi-Square Tests

chisq_labels chisq_values
smoking_status 2.008e-06
gender 0.5598
hypertension 1.689e-19
heart_disease 2.121e-21
ever_married 1.686e-14
work_type 5.409e-10
residence_type 0.2998

T-Tests

ttest_labels ttest_values
age 2.176e-95
glucose 2.373e-11
bmi 0.0003064

The features gender and residence type do not show a statistically significant association with the stroke outcome. This can be observed visually as well as by the Chi-Square tests.

Review distribution of numeric features

hist(strokes$bmi, main="bmi", xlab="bmi")

hist(strokes$avg_glucose_level, main="average glucose level", xlab="avg_glucose_level")

hist(strokes$age, main="age", xlab="age")

NA
NA

Both avg_glucose_level and bmi are right-skewed. To help correct this both variables will be replaced with their respective log values

strokes$bmi <- log(strokes$bmi)
strokes$avg_glucose_level <- log(strokes$avg_glucose_level)

hist(strokes$bmi)

hist(strokes$avg_glucose_level)

Since neither gender nor residence_type show any statistically significant correlation with the stroke outcome both features will be removed to avoid any unintentional modle complexity or noise

strokes <- strokes[-c(1,7)]

Prepare Data Sets for Model Usage

  • Split into train, validation and test datasets
  • Create balanced training data sets
  • Separate into features and labels
  • Scale Datasets
  • One-Hot Encode Datasets

# create train/test dataset as well as subset of train for cross-validation and evaluation usage
set.seed(123)
selection <- createDataPartition(strokes$stroke, p=0.8, list=FALSE)   
trainDataFull = strokes[selection,]    
testData = strokes[-selection,]
set.seed(123)
selection2 <- createDataPartition(trainDataFull$stroke, p=0.8, list=FALSE)
trainData = trainDataFull[selection2,]
valData = trainDataFull[-selection2,]

#create balanced training datasets

trainDataBalanced <- ROSE(stroke ~ ., data=trainData, p=0.5)$data
trainDataFullBalanced <- ROSE(stroke ~ ., data=trainDataFull, p=0.5)$data

# separate features and labels

valDataFeatures <- valData[-9]
valDataLabels <- valData[9]

testDataFeatures <- testData[-9]
testDataLabels <- testData[9]

trainDataFeatures <- trainData[-9]
trainDataLabels <- trainData[9]

trainDataFullFeatures <- trainDataFull[-9]
trainDataFullLabels <- trainDataFull[9]

trainDataBalancedFeatures <- trainDataBalanced[-9]
trainDataBalancedLabels <- trainDataBalanced[9]

trainDataFullBalancedFeatures <- trainDataFullBalanced[-9]
trainDataFullBalancedLabels <- trainDataFullBalanced[9]

# scale features data sets. testData scales with trainDataFull while valData scales with trainData

preProcScaleFullTrain <- preProcess(trainDataFullBalancedFeatures, method=c("center", "scale"))
preProcScaleTrain <- preProcess(trainDataBalancedFeatures, method=c("center", "scale"))

trainDataFullBalancedFeatures_s <- predict(preProcScaleFullTrain, trainDataFullBalancedFeatures)
testDataFeatures_s <- predict(preProcScaleFullTrain, testDataFeatures)

trainDataBalancedFeatures_s <- predict(preProcScaleTrain, trainDataBalancedFeatures)
valDataFeatures_s <- predict(preProcScaleTrain, valDataFeatures)

# one-hot encode  datasets
dummy1 <- dummyVars("~ .", data=trainDataFullBalancedFeatures_s)
trainDataFullBalancedFeatures_se <- data.frame(predict(dummy1,newdata=trainDataFullBalancedFeatures_s))

dummy2 <- dummyVars("~ .", data=trainDataBalancedFeatures)
trainDataBalancedFeatures_e <- data.frame(predict(dummy2,newdata=trainDataBalancedFeatures))

dummy3 <- dummyVars("~ .", data=trainDataFeatures)
trainDataFeatures_e <- data.frame(predict(dummy3,newdata=trainDataFeatures))

dummy4 <- dummyVars("~ .", data=trainDataFullFeatures)
trainDataFullFeatures_e <- data.frame(predict(dummy4,newdata=trainDataFullFeatures))


testDataFeatures_se <- data.frame(predict(dummy1,newdata=testDataFeatures_s))


valDataFeatures_e <- data.frame(predict(dummy2,newdata=valDataFeatures))

fullsize <- dim(trainDataFull)
trainsize <- dim(trainData)
valsize <- dim(valData)
testsize <- dim(testData)

distfull <- table(trainDataFull$stroke)
disttrain <- table(trainData$stroke)
distval <- table(valData$stroke)
disttest <- table(testData$stroke)

Verify dimensions and distribution of response variable for datasets

trainDataFull

4088 9

trainData

3271 9

valData

817 9

testData

1021 9

trainDataFull

No Yes
0.9511 0.04892

trainData

No Yes
0.9511 0.04891

valData

No Yes
0.951 0.04896

testData

No Yes
0.952 0.04799

The proportion of “no” to “yes” values in all datasets is equivalent.

Fit Models

Define control for models trained with Caret

myControl <- trainControl(method='repeatedcv',
                          number=10,
                          repeats=5,
                          verboseIter = FALSE,
                          classProbs=TRUE,
                          summaryFunction=twoClassSummary,
                          selectionFunction="oneSE",
                          sampling='rose')

myControl2 <- trainControl(method='repeatedcv',
                          number=10,
                          repeats=5,
                          verboseIter = FALSE,
                          classProbs=TRUE,
                          summaryFunction=twoClassSummary,
                          selectionFunction="oneSE")

KNN Model Training and Evaluation

Ensuring the square root of the number of observations is contained in the values of k being explored

set.seed(123)

knn_grid <- expand.grid(k=seq(1,100,by=2))

knn_train <- cbind(trainDataFeatures_e, trainDataLabels)
knn_model <- caret::train(stroke ~ ., data=knn_train, method='knn', trControl=myControl, tuneGrid = knn_grid, preProc = 'range', metric='ROC')


plot(knn_model)


knn_pred <- predict(knn_model, newdata=valDataFeatures_e)
knn_prob <- predict(knn_model, newdata=valDataFeatures_e, type='prob')
knn_results <- confusionMatrix(knn_pred, valDataLabels$stroke, positive = "Yes")

knn_results2 <- data.frame(as.factor(knn_pred), as.factor(valDataLabels$stroke), knn_prob)
colnames(knn_results2) <- c("pred", "obs", "No", "Yes")
knn_model
k-Nearest Neighbors 

3271 samples
  15 predictor
   2 classes: 'No', 'Yes' 

Pre-processing: re-scaling to [0, 1] (15) 
Resampling: Cross-Validated (10 fold, repeated 5 times) 
Summary of sample sizes: 2943, 2944, 2944, 2944, 2944, 2944, ... 
Addtional sampling using ROSE prior to pre-processing

Resampling results across tuning parameters:

  k   ROC        Sens       Spec   
   1  0.6105385  0.8048271  0.41625
   3  0.6951932  0.8315020  0.42375
   5  0.7247873  0.8163251  0.46125
   7  0.7548163  0.8279003  0.45875
   9  0.7566825  0.8289298  0.47500
  11  0.7614193  0.8243650  0.49000
  13  0.7684376  0.8212126  0.49750
  15  0.7703217  0.8217953  0.50625
  17  0.7678556  0.8244956  0.47125
  19  0.7788091  0.8132480  0.51250
  21  0.7773042  0.8133047  0.50125
  23  0.7771587  0.8135640  0.53500
  25  0.7789210  0.8165871  0.51375
  27  0.7874020  0.8105468  0.54000
  29  0.7852502  0.8106664  0.52125
  31  0.7866838  0.8080365  0.55000
  33  0.7889458  0.8060413  0.54125
  35  0.7820573  0.8046302  0.55500
  37  0.7818359  0.8024411  0.54125
  39  0.7847358  0.8050791  0.54125
  41  0.7870788  0.7989082  0.58000
  43  0.7808888  0.7982614  0.56250
  45  0.7808209  0.7917104  0.56625
  47  0.7874359  0.7952428  0.57250
  49  0.7784092  0.7935706  0.56750
  51  0.7859306  0.7906159  0.58875
  53  0.7840340  0.7929279  0.58125
  55  0.7859753  0.7889426  0.59125
  57  0.7896676  0.7908739  0.59125
  59  0.7863470  0.7875934  0.59125
  61  0.7871621  0.7915774  0.58625
  63  0.7889519  0.7896496  0.58625
  65  0.7865711  0.7832791  0.60250
  67  0.7867171  0.7789822  0.60750
  69  0.7893115  0.7787804  0.61375
  71  0.7886373  0.7805204  0.61500
  73  0.7825669  0.7715238  0.61000
  75  0.7852127  0.7722908  0.61750
  77  0.7892454  0.7787225  0.61125
  79  0.7843856  0.7742163  0.60875
  81  0.7882281  0.7762794  0.61625
  83  0.7847296  0.7742798  0.61125
  85  0.7900390  0.7643229  0.63500
  87  0.7892112  0.7690075  0.63750
  89  0.7883087  0.7713262  0.60875
  91  0.7825409  0.7645117  0.61250
  93  0.7864030  0.7708818  0.61625
  95  0.7885036  0.7717120  0.61500
  97  0.7855760  0.7701090  0.61875
  99  0.7874074  0.7652774  0.64750

ROC was used to select the optimal model using  the one SE rule.
The final value used for the model was k = 99.
knn_results
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  604  15
       Yes 173  25
                                          
               Accuracy : 0.7699          
                 95% CI : (0.7395, 0.7983)
    No Information Rate : 0.951           
    P-Value [Acc > NIR] : 1               
                                          
                  Kappa : 0.14            
                                          
 Mcnemar's Test P-Value : <2e-16          
                                          
            Sensitivity : 0.62500         
            Specificity : 0.77735         
         Pos Pred Value : 0.12626         
         Neg Pred Value : 0.97577         
             Prevalence : 0.04896         
         Detection Rate : 0.03060         
   Detection Prevalence : 0.24235         
      Balanced Accuracy : 0.70117         
                                          
       'Positive' Class : Yes             
                                          
knn_2class <- twoClassSummary(knn_results2, lev = c("No", "Yes"))
knn_2class[1]
      ROC 
0.7888996 
knn_roc <- roc(valDataLabels$stroke ~ knn_prob$Yes, plot=TRUE, print.auc=TRUE, col='black', lwd=4, legacy.axes=TRUE, main="ROC Curve", add=FALSE)
Setting levels: control = No, case = Yes
Setting direction: controls < cases

Logistic Regression Model with Elastic Net Regularization

set.seed(123)

glm_grid <- expand.grid(alpha=seq(0,1, length=10), lambda = 10^seq(-3,3, length=100))

glm_train <- cbind(trainDataFeatures_e, trainDataLabels)
glm_model <- caret::train(stroke ~ ., data=glm_train, method='glmnet', trControl=myControl, tuneGrid = glm_grid, preProc = "range", metric='ROC', family='binomial')


glm_pred <- predict(glm_model, newdata = valDataFeatures_e)
glm_prob <- predict(glm_model, newdata = valDataFeatures_e, type='prob')
glm_results <- confusionMatrix(glm_pred, valDataLabels$stroke, positive="Yes")

glm_results
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  551   6
       Yes 226  34
                                          
               Accuracy : 0.716           
                 95% CI : (0.6838, 0.7467)
    No Information Rate : 0.951           
    P-Value [Acc > NIR] : 1               
                                          
                  Kappa : 0.155           
                                          
 Mcnemar's Test P-Value : <2e-16          
                                          
            Sensitivity : 0.85000         
            Specificity : 0.70914         
         Pos Pred Value : 0.13077         
         Neg Pred Value : 0.98923         
             Prevalence : 0.04896         
         Detection Rate : 0.04162         
   Detection Prevalence : 0.31824         
      Balanced Accuracy : 0.77957         
                                          
       'Positive' Class : Yes             
                                          
glm_results2 <- data.frame(as.factor(glm_pred), as.factor(valDataLabels$stroke), glm_prob)


colnames(glm_results2) <- c("pred", "obs", "No", "Yes")



glm_2class <- twoClassSummary(glm_results2, lev=c("No", "Yes"))
glm_2class[1]
      ROC 
0.8555341 
glm_roc <- roc(valDataLabels$stroke ~ glm_prob$Yes, plot=TRUE, print.auc=TRUE, col='black', lwd=4, legacy.axes=TRUE, main="ROC Curve", add=FALSE)
Setting levels: control = No, case = Yes
Setting direction: controls < cases

coef(glm_model$finalModel,glm_model$bestTune$lambda)
16 x 1 sparse Matrix of class "dgCMatrix"
                                          1
(Intercept)                    -1.166219516
age                             1.480281231
hypertension                    0.333618442
heart_disease                   0.001788369
ever_married                    0.211458168
work_type.children             -0.249526776
work_type.Govt_job              .          
work_type.Never_worked          .          
work_type.Private               .          
work_type.Self_employed         .          
avg_glucose_level               0.124124895
bmi                             .          
smoking_status.formerly_smoked  .          
smoking_status.never_smoked     .          
smoking_status.smokes           .          
smoking_status.unknown          .          
varImp(glm_model)
glmnet variable importance

Decision Tree Model



set.seed(123)

tree_grid <- expand.grid(cp=seq(0.0,1.0,by=.001))



tree_data <- cbind(trainDataFeatures, trainDataLabels)
# tree_model <- caret::train(stroke ~.,data=tree_data, method='rpart', trControl=myControl, preProc = "range", metric='ROC', tuneGrid=tree_grid)
tree_model <- caret::train(stroke ~.,data=tree_data, method='rpart', trControl=myControl, metric='ROC', tuneGrid=tree_grid)




tree_pred <- predict(tree_model, valDataFeatures)
tree_prob <- predict(tree_model, newdata = valDataFeatures, type='prob')
tree_results <- confusionMatrix(tree_pred, valDataLabels$stroke, positive="Yes")
tree_results
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No    0   0
       Yes 777  40
                                          
               Accuracy : 0.049           
                 95% CI : (0.0352, 0.0661)
    No Information Rate : 0.951           
    P-Value [Acc > NIR] : 1               
                                          
                  Kappa : 0               
                                          
 Mcnemar's Test P-Value : <2e-16          
                                          
            Sensitivity : 1.00000         
            Specificity : 0.00000         
         Pos Pred Value : 0.04896         
         Neg Pred Value :     NaN         
             Prevalence : 0.04896         
         Detection Rate : 0.04896         
   Detection Prevalence : 1.00000         
      Balanced Accuracy : 0.50000         
                                          
       'Positive' Class : Yes             
                                          
tree_results2 <- data.frame(as.factor(tree_pred), as.factor(valDataLabels$stroke), tree_prob)


colnames(tree_results2) <- c("pred", "obs", "No", "Yes")



tree_2class <- twoClassSummary(tree_results2, lev=c("No", "Yes"))
tree_2class[1]
ROC 
0.5 
tree_roc <- roc(valDataLabels$stroke ~ tree_prob$Yes, plot=TRUE, print.auc=TRUE, col='black', lwd=4, legacy.axes=TRUE, main="ROC Curve", add=FALSE)
Setting levels: control = No, case = Yes
Setting direction: controls < cases

fancyRpartPlot(tree_model$finalModel)

Considering the poor performance of the decision tree, a second model will be built but will not balance the dataset prior to use



set.seed(123)

tree_grid <- expand.grid(cp=seq(0.0,1.0,by=.001))



tree_data <- cbind(trainDataFeatures, trainDataLabels)
tree_model <- caret::train(stroke ~.,data=tree_data, method='rpart', trControl=myControl2, preProc = "range", metric='ROC', tuneGrid=tree_grid)




tree_pred <- predict(tree_model, valDataFeatures)
tree_prob <- predict(tree_model, newdata = valDataFeatures, type='prob')
tree_results <- confusionMatrix(tree_pred, valDataLabels$stroke, positive="Yes")
tree_results
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  772  36
       Yes   5   4
                                          
               Accuracy : 0.9498          
                 95% CI : (0.9325, 0.9638)
    No Information Rate : 0.951           
    P-Value [Acc > NIR] : 0.6049          
                                          
                  Kappa : 0.1479          
                                          
 Mcnemar's Test P-Value : 2.797e-06       
                                          
            Sensitivity : 0.100000        
            Specificity : 0.993565        
         Pos Pred Value : 0.444444        
         Neg Pred Value : 0.955446        
             Prevalence : 0.048960        
         Detection Rate : 0.004896        
   Detection Prevalence : 0.011016        
      Balanced Accuracy : 0.546782        
                                          
       'Positive' Class : Yes             
                                          
tree_results2 <- data.frame(as.factor(tree_pred), as.factor(valDataLabels$stroke), tree_prob)


colnames(tree_results2) <- c("pred", "obs", "No", "Yes")



tree_2class <- twoClassSummary(tree_results2, lev=c("No", "Yes"))
tree_2class[1]
     ROC 
0.706435 
tree_roc <- roc(valDataLabels$stroke ~ tree_prob$Yes, plot=TRUE, print.auc=TRUE, col='black', lwd=4, legacy.axes=TRUE, main="ROC Curve", add=FALSE)
Setting levels: control = No, case = Yes
Setting direction: controls < cases

fancyRpartPlot(tree_model$finalModel)

Random Forest Model


rf_grid <- expand.grid(mtry=c(1,2,4,6,8))
set.seed(123)

rf_data <- cbind(trainDataFeatures, trainDataLabels)
rf_model <- caret::train(stroke ~.,data=rf_data, method='rf', trControl=myControl, preProc = "range", metric='ROC', tuneGrid=rf_grid)

rf_pred <- predict(rf_model, valDataFeatures)
rf_prob <- predict(rf_model, valDataFeatures, type='prob')
rf_results <- confusionMatrix(rf_pred,valDataLabels$stroke, positive="Yes")
rf_results
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  416   2
       Yes 361  38
                                          
               Accuracy : 0.5557          
                 95% CI : (0.5209, 0.5901)
    No Information Rate : 0.951           
    P-Value [Acc > NIR] : 1               
                                          
                  Kappa : 0.0923          
                                          
 Mcnemar's Test P-Value : <2e-16          
                                          
            Sensitivity : 0.95000         
            Specificity : 0.53539         
         Pos Pred Value : 0.09524         
         Neg Pred Value : 0.99522         
             Prevalence : 0.04896         
         Detection Rate : 0.04651         
   Detection Prevalence : 0.48837         
      Balanced Accuracy : 0.74270         
                                          
       'Positive' Class : Yes             
                                          
rf_results2 <- data.frame(as.factor(rf_pred), as.factor(valDataLabels$stroke), rf_prob)
colnames(rf_results2) <- c("pred", "obs", "No", "Yes")


rf_2class <- twoClassSummary(rf_results2, lev=c("No", "Yes"))
rf_2class[1]
      ROC 
0.8387227 
rf_roc <- roc(valDataLabels$stroke ~ rf_prob$Yes, plot=TRUE, print.auc=TRUE, col='black', lwd=4, legacy.axes=TRUE, main="ROC Curve")
Setting levels: control = No, case = Yes
Setting direction: controls < cases

varImp(rf_model, scale = TRUE)
rf variable importance
rf_model
Random Forest 

3271 samples
   8 predictor
   2 classes: 'No', 'Yes' 

Pre-processing: re-scaling to [0, 1] (13) 
Resampling: Cross-Validated (10 fold, repeated 5 times) 
Summary of sample sizes: 2943, 2944, 2944, 2944, 2944, 2944, ... 
Addtional sampling using ROSE prior to pre-processing

Resampling results across tuning parameters:

  mtry  ROC        Sens         Spec   
  1     0.8147439  0.539185011  0.92125
  2     0.8100990  0.210691112  0.99125
  4     0.7847946  0.004501195  1.00000
  6     0.7651958  0.004501195  1.00000
  8     0.7474645  0.004501195  1.00000

ROC was used to select the optimal model using  the one SE rule.
The final value used for the model was mtry = 1.
plot(rf_model)

Linear SVM Model

set.seed(123)
lsvm_grid <- expand.grid(C = 3**(-7:7))
lsvm_data <- cbind(trainDataFeatures_e, trainDataLabels)
lsvm_model <- caret::train(stroke ~., data = lsvm_data, method = "svmLinear", trControl = myControl, preProcess = 'range', tuneGrid = lsvm_grid, metric= 'ROC')
lsvm_pred <- predict(lsvm_model, valDataFeatures_e)
lsvm_prob <- predict(lsvm_model, valDataFeatures_e, type='prob')
lsvm_results <- confusionMatrix(lsvm_pred, valDataLabels$stroke, positive="Yes")
lsvm_results2 <- data.frame(as.factor(lsvm_pred), as.factor(valDataLabels$stroke), lsvm_prob)
lsvm_results
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  584   7
       Yes 193  33
                                          
               Accuracy : 0.7552          
                 95% CI : (0.7242, 0.7843)
    No Information Rate : 0.951           
    P-Value [Acc > NIR] : 1               
                                          
                  Kappa : 0.1799          
                                          
 Mcnemar's Test P-Value : <2e-16          
                                          
            Sensitivity : 0.82500         
            Specificity : 0.75161         
         Pos Pred Value : 0.14602         
         Neg Pred Value : 0.98816         
             Prevalence : 0.04896         
         Detection Rate : 0.04039         
   Detection Prevalence : 0.27662         
      Balanced Accuracy : 0.78830         
                                          
       'Positive' Class : Yes             
                                          
colnames(lsvm_results2) <- c("pred", "obs", "No", "Yes")
lsvm_2class <- twoClassSummary(lsvm_results2, lev=c("No", "Yes"))
lsvm_2class[1]
      ROC 
0.8487773 
varImp(lsvm_model, scale = TRUE)
ROC curve variable importance
lsvm_roc <- roc(valDataLabels$stroke ~ lsvm_prob$Yes, plot=TRUE, print.auc=TRUE, col='black', lwd=4, legacy.axes=TRUE, main="ROC Curve", add=FALSE)
Setting levels: control = No, case = Yes
Setting direction: controls < cases

plot(lsvm_model, scales=list(x=list(log=3)))

lsvm_model
Support Vector Machines with Linear Kernel 

3271 samples
  15 predictor
   2 classes: 'No', 'Yes' 

Pre-processing: re-scaling to [0, 1] (15) 
Resampling: Cross-Validated (10 fold, repeated 5 times) 
Summary of sample sizes: 2943, 2944, 2944, 2944, 2944, 2944, ... 
Addtional sampling using ROSE prior to pre-processing

Resampling results across tuning parameters:

  C             ROC        Sens       Spec   
  4.572474e-04  0.8223611  0.7261314  0.77000
  1.371742e-03  0.8305492  0.7266456  0.79875
  4.115226e-03  0.8371825  0.7276736  0.81625
  1.234568e-02  0.8363893  0.7294760  0.80875
  3.703704e-02  0.8366878  0.7251682  0.81625
  1.111111e-01  0.8377090  0.7283768  0.80875
  3.333333e-01  0.8391237  0.7261312  0.81250
  1.000000e+00  0.8384358  0.7249744  0.81375
  3.000000e+00  0.8372659  0.7269680  0.81125
  9.000000e+00  0.8369120  0.7234337  0.80875
  2.700000e+01  0.8387552  0.7233682  0.81625
  8.100000e+01  0.8389665  0.7263268  0.81625
  2.430000e+02  0.8384265  0.7249740  0.81625
  7.290000e+02  0.8341659  0.7413742  0.78125
  2.187000e+03  0.8366235  0.7315947  0.81000

ROC was used to select the optimal model using  the one SE rule.
The final value used for the model was C = 0.004115226.

Gradient Boost Model

gbm_Grid <-  expand.grid(interaction.depth = c(1, 3, 6, 9, 10),
                    n.trees = seq(250,2000,250), 
                    shrinkage = seq(.0005, .05,.0005),
                    n.minobsinnode = 10)

set.seed(123)
gbm_data = cbind(trainDataFeatures_e, trainDataLabels)
gbm_model <- caret::train(stroke ~., data = gbm_data, method = "gbm", trControl = myControl, preProcess = 'range', metric= 'ROC', tuneLength = 10)
gbm_pred <- predict(gbm_model, valDataFeatures_e)
gbm_prob <- predict(gbm_model, valDataFeatures_e, type='prob')

gbm_results <- confusionMatrix(gbm_pred, valDataLabels$stroke, positive='Yes')

gbm_results
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No    3   0
       Yes 774  40
                                          
               Accuracy : 0.0526          
                 95% CI : (0.0383, 0.0702)
    No Information Rate : 0.951           
    P-Value [Acc > NIR] : 1               
                                          
                  Kappa : 4e-04           
                                          
 Mcnemar's Test P-Value : <2e-16          
                                          
            Sensitivity : 1.000000        
            Specificity : 0.003861        
         Pos Pred Value : 0.049140        
         Neg Pred Value : 1.000000        
             Prevalence : 0.048960        
         Detection Rate : 0.048960        
   Detection Prevalence : 0.996328        
      Balanced Accuracy : 0.501931        
                                          
       'Positive' Class : Yes             
                                          
gbm_results2 <- data.frame(as.factor(gbm_pred), as.factor(valDataLabels$stroke), gbm_prob)


colnames(gbm_results2) <- c("pred", "obs", "No", "Yes")



gbm_2class <- twoClassSummary(gbm_results2, lev=c("No", "Yes"))
gbm_2class[1]
      ROC 
0.6796493 
gbm_roc <- roc(valDataLabels$stroke ~ gbm_prob$Yes, plot=TRUE, print.auc=TRUE, col='black', lwd=4, legacy.axes=TRUE, main="ROC Curve", add=FALSE)
Setting levels: control = No, case = Yes
Setting direction: controls < cases

gbm_model
Stochastic Gradient Boosting 

3271 samples
  15 predictor
   2 classes: 'No', 'Yes' 

Pre-processing: re-scaling to [0, 1] (15) 
Resampling: Cross-Validated (10 fold, repeated 5 times) 
Summary of sample sizes: 2943, 2944, 2944, 2944, 2944, 2944, ... 
Addtional sampling using ROSE prior to pre-processing

Resampling results across tuning parameters:

  interaction.depth  n.trees  ROC        Sens        Spec   
   1                  50      0.5120476  0.01311856  0.99625
   1                 100      0.5120396  0.01614107  0.99500
   1                 150      0.5284698  0.01311856  0.99625
   1                 200      0.5718248  0.01909927  0.99375
   1                 250      0.5915129  0.01909927  0.99375
   1                 300      0.6157874  0.01607676  0.99500
   1                 350      0.6248188  0.01607676  0.99500
   1                 400      0.6370812  0.01607676  0.99500
   1                 450      0.6420189  0.01909927  0.99375
   1                 500      0.6442202  0.01607676  0.99500
   2                  50      0.5164203  0.01909927  0.99375
   2                 100      0.5623875  0.01909927  0.99375
   2                 150      0.5850932  0.01909927  0.99375
   2                 200      0.5986393  0.01909927  0.99375
   2                 250      0.6160540  0.01916357  0.99375
   2                 300      0.6317473  0.01922788  0.99375
   2                 350      0.6487803  0.01646261  0.99500
   2                 400      0.6537964  0.01652692  0.99500
   2                 450      0.6606227  0.01652692  0.99500
   2                 500      0.6647613  0.01652692  0.99500
   3                  50      0.5114126  0.01909927  0.99375
   3                 100      0.5378068  0.01909927  0.99375
   3                 150      0.5664027  0.01909927  0.99375
   3                 200      0.5855627  0.01909927  0.99375
   3                 250      0.5919699  0.01916357  0.99375
   3                 300      0.6096614  0.01916357  0.99375
   3                 350      0.6172812  0.01916357  0.99375
   3                 400      0.6210599  0.01929219  0.99375
   3                 450      0.6283093  0.01929219  0.99375
   3                 500      0.6380882  0.01929219  0.99375
   4                  50      0.5192763  0.01909927  0.99375
   4                 100      0.5415156  0.01909927  0.99375
   4                 150      0.5633766  0.01909927  0.99375
   4                 200      0.5706053  0.01909927  0.99375
   4                 250      0.5790300  0.01909927  0.99375
   4                 300      0.5897672  0.01909927  0.99375
   4                 350      0.6253183  0.01922788  0.99375
   4                 400      0.6463812  0.01922788  0.99375
   4                 450      0.6583945  0.01922788  0.99375
   4                 500      0.6624801  0.01922788  0.99375
   5                  50      0.4982885  0.01909927  0.99375
   5                 100      0.5213421  0.01909927  0.99375
   5                 150      0.5531265  0.01909927  0.99375
   5                 200      0.5872584  0.01909927  0.99375
   5                 250      0.6011820  0.01909927  0.99375
   5                 300      0.6084796  0.01967660  0.99375
   5                 350      0.6348726  0.01993301  0.99375
   5                 400      0.6606058  0.02006122  0.99375
   5                 450      0.6667146  0.02044666  0.99375
   5                 500      0.6759749  0.02057527  0.99375
   6                  50      0.5229865  0.01909927  0.99375
   6                 100      0.5431204  0.01909927  0.99375
   6                 150      0.5414926  0.01909927  0.99375
   6                 200      0.5618280  0.01909927  0.99375
   6                 250      0.5709914  0.01909927  0.99375
   6                 300      0.5918941  0.01916357  0.99375
   6                 350      0.6107437  0.01916357  0.99375
   6                 400      0.6242674  0.01942081  0.99375
   6                 450      0.6414063  0.02173592  0.99375
   6                 500      0.6391775  0.02135007  0.99375
   7                  50      0.4739808  0.01909927  0.99375
   7                 100      0.4692592  0.01909927  0.99375
   7                 150      0.4962372  0.01909927  0.99375
   7                 200      0.5336867  0.01909927  0.99375
   7                 250      0.5601293  0.02135007  0.99375
   7                 300      0.5752217  0.02154300  0.99375
   7                 350      0.5990514  0.02212177  0.99375
   7                 400      0.6127874  0.02270055  0.99375
   7                 450      0.6220114  0.02334364  0.99375
   7                 500      0.6380094  0.02372949  0.99375
   8                  50      0.4832537  0.01909927  0.99375
   8                 100      0.5032918  0.01909927  0.99375
   8                 150      0.5346193  0.01909927  0.99375
   8                 200      0.5394732  0.01916357  0.99375
   8                 250      0.5726730  0.01935650  0.99375
   8                 300      0.5902557  0.01980666  0.99375
   8                 350      0.6110737  0.01993528  0.99375
   8                 400      0.6203771  0.01999959  0.99375
   8                 450      0.6540952  0.02012821  0.99375
   8                 500      0.6631274  0.02019251  0.99375
   9                  50      0.5291433  0.01909927  0.99375
   9                 100      0.5304313  0.01909927  0.99375
   9                 150      0.5330051  0.01909927  0.99375
   9                 200      0.5486256  0.01909927  0.99375
   9                 250      0.5942750  0.02295779  0.99375
   9                 300      0.6119054  0.02398673  0.99375
   9                 350      0.6214457  0.02572079  0.99375
   9                 400      0.6257709  0.02655578  0.99375
   9                 450      0.6386572  0.02668419  0.99375
   9                 500      0.6522057  0.02674829  0.99375
  10                  50      0.5091538  0.01909927  0.99375
  10                 100      0.5384197  0.01909927  0.99375
  10                 150      0.5509515  0.01909927  0.99375
  10                 200      0.5710913  0.01909927  0.99375
  10                 250      0.5958467  0.01909927  0.99375
  10                 300      0.6124699  0.02077047  0.99375
  10                 350      0.6356838  0.02250495  0.99375
  10                 400      0.6526590  0.02282649  0.99375
  10                 450      0.6743980  0.02417615  0.99375
  10                 500      0.6833120  0.02417615  0.99375

Tuning parameter 'shrinkage' was held constant at a value of 0.1
Tuning parameter 'n.minobsinnode' was held constant at a value of 10
ROC was used to select the optimal model using  the one SE rule.
The final values used for the model were n.trees = 450, interaction.depth = 10, shrinkage = 0.1 and n.minobsinnode = 10.
plot(gbm_model)

Neural Network Model

Scale datasets for usage


numerical <- c(1,6,7)


trainDataFullNum <- trainDataFull[numerical]
trainDataFullCat <- trainDataFull[-numerical]



trainDataNum <- trainData[numerical]
trainDataCat <- trainData[-numerical]


valDataNum <- valData[numerical]
valDataCat <- valData[-numerical]


testDataNum <- testData[numerical]
testDataCat <- testData[-numerical]

trainDataFullScaled <- scale(trainDataFullNum)
col_means_train <- attr(trainDataFullScaled, "scaled:center")
col_stddevs_train <- attr(trainDataFullScaled, "scaled:scale")

testDataScaled <- scale(testDataNum, center = col_means_train, scale = col_stddevs_train)  

trainDataScaled <- scale(trainDataNum)
col_means_train <- attr(trainDataScaled, "scaled:center")
col_stddevs_train <- attr(trainDataScaled, "scaled:scale")

valDataScaled <- scale(valDataNum, center = col_means_train, scale = col_stddevs_train)  

nnFullTrainData <- cbind(trainDataFullScaled, trainDataFullCat)
nnTrainData <- cbind(trainDataScaled, trainDataCat)
nnTestData <- cbind(testDataScaled, testDataCat)
nnValData <- cbind(valDataScaled, valDataCat)


# balance training data

nnTrainData <- ROSE(stroke ~ ., data=nnTrainData, p=0.5)$data

nnFullTrainData <- ROSE(stroke ~ ., data=nnFullTrainData, p=0.5)$data

nnValBal <- ROSE(stroke ~ ., data=nnValData, p=0.5)$data

separate features from labels

nnFullTrainFeatures <- nnFullTrainData[-9]
nnFullTrainLabels <- nnFullTrainData[9]

nnTrainFeatures <- nnTrainData[-9]
nnTrainLabels <- nnTrainData[9]

nnTestFeatures <- nnTestData[-9]
nnTestLabels <- nnTestData[9]

nnValFeatures <- nnValData[-9]
nnValLabels <- nnValData[9]

nnValBalFeatures <- nnValBal[-9]
nnValBalLabels <- nnValBal[9]

nnValLabels$stroke <- ifelse(nnValLabels$stroke == 'No', 0, 1)
nnTestLabels$stroke <- ifelse(nnTestLabels$stroke == 'No', 0, 1)
nnTrainLabels$stroke <- ifelse(nnTrainLabels$stroke == 'No', 0, 1)
nnFullTrainLabels$stroke <- ifelse(nnFullTrainLabels$stroke == 'No', 0, 1)
nnValBalLabels$stroke <- ifelse(nnValBalLabels$stroke == 'No', 0, 1)

### Stroke Distrubution for Train Dataset

-------------
  0      1   
------ ------
 1642   1629 
-------------


### Stroke Distrubution for fullTrain Dataset

-------------
  0      1   
------ ------
 2021   2067 
-------------

one-hot encode categorical features


dummyFullTrain <- dummyVars("~ .", data=nnFullTrainFeatures) 
dummyTrain <- dummyVars("~ .", data=nnTrainFeatures)

nnFullTrainFeatures <- data.frame(predict(dummyFullTrain,newdata=nnFullTrainFeatures))
nnTestFeatures <- data.frame(predict(dummyFullTrain,newdata=nnTestFeatures))

nnTrainFeatures <- data.frame(predict(dummyTrain,newdata=nnTrainFeatures))
nnValFeatures <- data.frame(predict(dummyTrain,newdata=nnValFeatures))
nnValBalFeatures <- data.frame(predict(dummyTrain,newdata=nnValBalFeatures))

Neural Network Model Tuning

 
clean_runs(confirm=FALSE)
set.seed(123)
runs <- tuning_run("ann.R", flags = list(nodes1 = c(20, 50,75), nodes2=c(20,50,75), drop1=c(0.2,0.4,0.6), drop2=c(0.2,0.4,0.6), learning_rate = c(0.01,0.05,0.001,0.0001), epochs = c(25,50,100), batch_size = c(10,25,50), activation = c("relu", "sigmoid", "tanh")),sample = 0.02, confirm=FALSE, echo = FALSE)
rundf <- ls_runs(order='metric_val_accuracy', decreasing=TRUE)
rundf 
Data frame: 175 x 28 
# ... with 165 more rows
# ... with 23 more columns:
#   flag_nodes1, flag_nodes2, flag_batch_size, flag_activation, flag_learning_rate,
#   flag_epochs, flag_drop1, flag_drop2, epochs, epochs_completed, metrics, model,
#   loss_function, optimizer, learning_rate, script, start, end, completed, output,
#   source_code, context, type
bestrun <- rundf[1,1:19]

Best Run Details

  62
run_dir runs/2021-04-28T05-06-54Z
metric_val_accuracy 0.951
metric_loss 0.6952
metric_accuracy 0.5032
metric_val_loss 0.6749
flag_nodes1 50
flag_nodes2 50
flag_batch_size 10
flag_activation relu
flag_learning_rate 0.05
flag_epochs 50
flag_drop1 0.4
flag_drop2 0.6
epochs 50
epochs_completed 50
metrics runs/2021-04-28T05-06-54Z/tfruns.d/metrics.json
model Model Model: “sequential” ________________________________________________________________________________ Layer (type) Output Shape Param # ================================================================================ dense_2 (Dense) (None, 50) 800 ________________________________________________________________________________ dropout_1 (Dropout) (None, 50) 0 ________________________________________________________________________________ dense_1 (Dense) (None, 50) 2550 ________________________________________________________________________________ dropout (Dropout) (None, 50) 0 ________________________________________________________________________________ dense (Dense) (None, 1) 51 ================================================================================ Total params: 3,401 Trainable params: 3,401 Non-trainable params: 0 ________________________________________________________________________________
loss_function binary_crossentropy
optimizer <tensorflow.python.keras.optimizer_v2.adam.Adam>

Create model using paramters from best tuning run

model <- keras_model_sequential()
model %>%
  layer_dense(units = 50, activation = "relu", input_shape = dim(nnTrainFeatures)[2]) %>%
  layer_dropout(rate=0.4) %>%
  layer_dense(units = 50, activation = "relu") %>%
  layer_dropout(rate=0.6) %>%
  layer_dense(units = 1, activation = 'sigmoid')

model %>% compile(
  optimizer = optimizer_adam(lr=0.05),
  
  loss='binary_crossentropy',
  metrics = c('accuracy')
 
)

set.seed(234)
history <- model %>% fit(as.matrix(nnTrainFeatures), as.matrix(nnTrainLabels), epochs=50, batch_size=10,validation_data=list(as.matrix(nnValFeatures), as.matrix(nnValLabels)))

Evaluate NN Model Performance

set.seed(123)
model %>% evaluate(as.matrix(nnValFeatures), as.matrix(nnValLabels))

 1/26 [>.............................] - ETA: 0s - loss: 0.4610 - accuracy: 0.5000
26/26 [==============================] - 0s 627us/step - loss: 0.4428 - accuracy: 0.4908
     loss 
26/26 [==============================] - 0s 634us/step - loss: 0.4428 - accuracy: 0.4908
 accuracy 
0.4427884 0.4908201 
nn_pred <- model %>% predict_classes(as.matrix(nnValFeatures)) 
nn_prob <- model %>% predict_proba(as.matrix(nnValFeatures))
nn_prob <- data.frame(nn_prob)
colnames(nn_prob) <- c('Yes')
nn_prob$No <- 1 - nn_prob$Yes
nn_results2 <- data.frame(nn_pred, nnValLabels, nn_prob) 

colnames(nn_results2) <- c("pred", "obs", "Yes", "No")
nn_results2$pred <-as.factor(ifelse(nn_results2$pred == 0, 'No', 'Yes'))
nn_results2$obs <-as.factor(ifelse(nn_results2$obs == 0, 'No', 'Yes'))
nn_2class <- twoClassSummary(nn_results2, lev=c("No", "Yes"))
confusionMatrix(nn_results2$pred,nn_results2$obs, positive='Yes')
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  361   0
       Yes 416  40
                                         
               Accuracy : 0.4908         
                 95% CI : (0.456, 0.5257)
    No Information Rate : 0.951          
    P-Value [Acc > NIR] : 1              
                                         
                  Kappa : 0.0783         
                                         
 Mcnemar's Test P-Value : <2e-16         
                                         
            Sensitivity : 1.00000        
            Specificity : 0.46461        
         Pos Pred Value : 0.08772        
         Neg Pred Value : 1.00000        
             Prevalence : 0.04896        
         Detection Rate : 0.04896        
   Detection Prevalence : 0.55814        
      Balanced Accuracy : 0.73230        
                                         
       'Positive' Class : Yes            
                                         
nn_2class[1]
      ROC 
0.7342342 
plot(history)

nn_roc <- roc(nnValLabels$stroke ~ nn_prob$Yes, plot=TRUE, print.auc=TRUE, col='black', lwd=4, legacy.axes=TRUE, main="ROC Curve", add=FALSE)
Setting levels: control = 0, case = 1
Setting direction: controls < cases

Model Comparison

Model Performace Comparison

  ROC Spec Sens
knn 0.7889 0.7773 0.625
glmnet 0.8555 0.7091 0.85
rpart 0.7064 0.9936 0.1
rf 0.8387 0.5354 0.95
svmLinear 0.8488 0.7516 0.825
gbm 0.6796 0.003861 1
neural network 0.7342 0.4646 1
pred_rf= prediction(rf_prob$Yes,valDataLabels$stroke)

performance(pred_rf, measure = "auc")@y.values
rf_roc <- roc(valDataLabels$stroke ~ rf_prob$Yes, plot=TRUE, print.auc=FALSE, col='green', lwd=4, legacy.axes=TRUE, main="ROC Curve")
pred_knn = prediction(knn_prob$Yes, valDataLabels$stroke)
performance(pred_knn, measure='auc')@y.values
knn_roc <- roc(valDataLabels$stroke ~ knn_prob$Yes, plot=TRUE, print.auc=FALSE, col='blue', lwd=4, legacy.axes=TRUE, main="ROC Curve", add=TRUE)
pred_glm <- prediction(glm_prob$Yes, valDataLabels$stroke)
glm_perform <- performance(pred_glm, measure='auc')@y.values
glm_roc <- roc(valDataLabels$stroke ~ glm_prob$Yes, plot=TRUE, print.auc=FALSE, col='orange', lwd=4, legacy.axes=TRUE, main="ROC Curve", add=TRUE)
pred_tree <- prediction(tree_prob$Yes, valDataLabels$stroke)
performance(pred_tree, measure='auc')@y.values
tree_roc <- roc(valDataLabels$stroke ~ tree_prob$Yes, plot=TRUE, print.auc=FALSE, col='yellow', lwd=4, legacy.axes=TRUE, main="ROC Curve", add=TRUE)
pred_lsvm <- prediction(lsvm_prob$Yes, valDataLabels$stroke)
performance(pred_lsvm, measure='auc')@y.values
lsvm_roc <- roc(valDataLabels$stroke ~ lsvm_prob$Yes, plot=TRUE, print.auc=FALSE, col='purple', lwd=4, legacy.axes=TRUE, main="ROC Curve", add=TRUE)
pred_gbm <- prediction(gbm_prob$Yes, valDataLabels$stroke)
performance(pred_gbm, measure='auc')@y.values
gbm_roc <- roc(valDataLabels$stroke ~ gbm_prob$Yes, plot=TRUE, print.auc=FALSE, col='black', lwd=4, legacy.axes=TRUE, main="ROC Curve", add=TRUE)
pred_nn = prediction(nn_prob$Yes,nnValLabels$stroke)
performance(pred_nn, measure='auc')@y.values
nn_roc <- roc(nnValLabels$stroke ~ nn_prob$Yes, plot=TRUE, print.auc=FALSE, col='brown', lwd=4, legacy.axes=TRUE, main="ROC Curve", add=TRUE)
legend("bottomright", legend=c("knn", 'rf', 'glmnet', 'rpart', 'svmLinear', 'gbm', 'neural network'), col=c("blue", "green", 'orange', 'yellow', 'purple', 'black', 'brown'), lwd=2)

Build Final Model

Based upon Train/Validation runs of the models, the Elastic Net model having ROC: 0.8493, Specificity: 0.7079 and Sensitivity: 0.8500 showed the best performance. We will now train a glmnet model using the full training data set and evaluate it using the test dataset.

fullData <- cbind(trainDataFullBalancedFeatures_se, trainDataFullBalancedLabels)

final_model <- glmnet(x=as.matrix(trainDataFullBalancedFeatures_se), y=as.matrix(trainDataFullBalancedLabels), family='binomial', alpha=glm_model$bestTune$alpha, lambda=glm_model$bestTune$lambda)
final_prediction <- predict(final_model, as.matrix(testDataFeatures_se),type='class')
final_probs <- predict(final_model, as.matrix(testDataFeatures_se),type='response')
final_prediction <- as.factor(final_prediction)
final_results <- data.frame(final_prediction, testDataLabels, final_probs)
colnames(final_results) <- c("pred", "obs", "Yes")
final_results$No <- 1- final_results$Yes

pred_final= prediction(final_results$Yes,testDataLabels$stroke)

performance(pred_final, measure = "auc")@y.values
[[1]]
[1] 0.8255018
final_roc <- roc(testDataLabels$stroke ~ final_results$Yes, plot=TRUE, 
                 print.auc=TRUE, col='red', lwd=4, legacy.axes=TRUE, main="ROC Curve")
Setting levels: control = No, case = Yes
Setting direction: controls < cases

confusionMatrix(final_prediction, testDataLabels$stroke, positive="Yes")
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  610   5
       Yes 362  44
                                        
               Accuracy : 0.6405        
                 95% CI : (0.6103, 0.67)
    No Information Rate : 0.952         
    P-Value [Acc > NIR] : 1             
                                        
                  Kappa : 0.1179        
                                        
 Mcnemar's Test P-Value : <2e-16        
                                        
            Sensitivity : 0.89796       
            Specificity : 0.62757       
         Pos Pred Value : 0.10837       
         Neg Pred Value : 0.99187       
             Prevalence : 0.04799       
         Detection Rate : 0.04310       
   Detection Prevalence : 0.39765       
      Balanced Accuracy : 0.76277       
                                        
       'Positive' Class : Yes           
                                        
coef(final_model)
16 x 1 sparse Matrix of class "dgCMatrix"
                                        s0
(Intercept)                     0.03373739
age                             0.28096432
hypertension                    0.02208788
heart_disease                   0.04157673
ever_married                    0.04646855
work_type.children             -0.13217010
work_type.Govt_job              .         
work_type.Never_worked          .         
work_type.Private               .         
work_type.Self_employed         .         
avg_glucose_level               0.02937247
bmi                             .         
smoking_status.formerly_smoked  .         
smoking_status.never_smoked     .         
smoking_status.smokes           .         
smoking_status.unknown          .         

Final metrics for the glmnet model are consistent with what was found during the cross-validation and tuning process

LS0tCnRpdGxlOiAiU3Ryb2tlIFByZXZlbnRpb24iCnN1YnRpdGxlOiAiQ1NDNTMyQiBTcHJpbmcgMjAyMSIKYXV0aG9yOiAiVGhvbWFzIENoYW1waW9uIgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCgpgYGB7ciBpbmNsdWRlPUZBTFNFfQpyZXF1aXJlKHBseXIpCnJlcXVpcmUoZHBseXIpCnJlcXVpcmUocGFuZGVyKQpyZXF1aXJlKGdncGxvdDIpCnJlcXVpcmUoZ3JpZEV4dHJhKQpyZXF1aXJlKGNhcmV0KQpyZXF1aXJlKERNd1IpCnJlcXVpcmUoUk9TRSkKcmVxdWlyZShrZXJhcykKdXNlX3Nlc3Npb25fd2l0aF9zZWVkKDQyKQpyZXF1aXJlKHRlbnNvcmZsb3cpCnJlcXVpcmUodGZydW5zKQpyZXF1aXJlKFJPQ1IpCnJlcXVpcmUocFJPQykKcmVxdWlyZShPbmVSKQpyZXF1aXJlKGdsbW5ldCkKcmVxdWlyZShyYXR0bGUpCnJlcXVpcmUocnBhcnQpCnJlcXVpcmUocnBhcnQucGxvdCkKcmVxdWlyZShnYm0pCnJlcXVpcmUoeGdib29zdCkKYGBgCgojIExvYWQgYW5kIEV4cGxvcmUgRGF0YQpgYGB7cn0Kc3Ryb2tlcyA8LSByZWFkLmNzdigiLi9kYXRhL3N0cm9rZV9kYXRhLmNzdiIpCnN0cihzdHJva2VzKQpzdW1tYXJ5KHN0cm9rZXMpCmBgYAoKCgoKIyMjIFZlcmlmeSB0aGF0IHRoZSBJRCBjb2x1bW4gaXMgaW4gZmFjdCB1bmlxdWUKIyMjIFJlbW92ZSBJRCBjb2x1bW4gc2luY2UgdGhpcyBpcyBhbiBhc3NpZ25lZCBpZGVudGlmaWVyIGFuZCBhcyBzdWNoIGRvZXMgbm90IGltcGFjdCBzdHJva2UKCmBgYHtyfQp1bmlxdWVJRCA8LSB1bmlxdWUoc3Ryb2tlcyRpZCkKbGVuZ3RoKHVuaXF1ZUlEKQpkaW0oc3Ryb2tlcykKc3Ryb2tlcyA8LSBzdHJva2VzWy0xXQpgYGAKCiMjIyBTaW5jZSBhbGwgdGhlIHBvc2l0aXZlIHN0cm9rZSB2YWx1ZXMgYXJlIGZvdW5kIGF0IHRoZSBzdGFydCBvZiB0aGUgZmlsZSB3ZSB3aWxsIHJhbmRvbWl6ZSB0aGUgZGF0YWZyYW1lCgpgYGB7cn0Kc2V0LnNlZWQoNDIpCnJvd3MgPC0gc2FtcGxlKG5yb3coc3Ryb2tlcykpCnN0cm9rZXMgPC0gc3Ryb2tlc1tyb3dzLF0KYGBgCgojIyMgUmV2aWV3IGNhdGVnb3J5IGRpc3RydWJ1dGlvbiBmb3IgY2F0ZWdvcmljYWwgdmFyaWFibGVzCmBgYHtyIGVjaG89RkFMU0UsIHJlc3VsdHM9J2FzaXMnfQpwYW5kb2MuaGVhZGVyKCJHZW5kZXIgRW50cmllcyIsNSkKcGFuZG9jLnRhYmxlKHRhYmxlKHN0cm9rZXMkZ2VuZGVyKSkKCnBhbmRvYy5oZWFkZXIoIkV2ZXIgTWFycmllZCBFbnRyaWVzIiw1KQpwYW5kb2MudGFibGUodGFibGUoc3Ryb2tlcyRldmVyX21hcnJpZWQpKQoKcGFuZG9jLmhlYWRlcigiV29yayBUeXBlIEVudHJpZXMiLDUpCnBhbmRvYy50YWJsZSh0YWJsZShzdHJva2VzJHdvcmtfdHlwZSkpCgpwYW5kb2MuaGVhZGVyKCJSZXNpZGVuY2UgVHlwZUVudHJpZXMiLDUpCnBhbmRvYy50YWJsZSh0YWJsZShzdHJva2VzJFJlc2lkZW5jZV90eXBlKSkKCnBhbmRvYy5oZWFkZXIoIlNtb2tpbmcgU3RhdHVzIEVudHJpZXMiLDUpCnBhbmRvYy50YWJsZSh0YWJsZShzdHJva2VzJHNtb2tpbmdfc3RhdHVzKSkKCnBhbmRvYy5oZWFkZXIoIkh5cGVydGVuc2lvbkVudHJpZXMiLDUpCnBhbmRvYy50YWJsZSh0YWJsZShzdHJva2VzJGh5cGVydGVuc2lvbikpCgpwYW5kb2MuaGVhZGVyKCJIZWFydCBEaXNlYXNlIEVudHJpZXMiLDUpCnBhbmRvYy50YWJsZSh0YWJsZShzdHJva2VzJGhlYXJ0X2Rpc2Vhc2UpKQoKCgpwYW5kb2MuaGVhZGVyKCJTbW9raW5nIFN0YXR1cyBFbnRyaWVzIiw1KQpwYW5kb2MudGFibGUodGFibGUoc3Ryb2tlcyRzbW9raW5nX3N0YXR1cykpCgpwYW5kb2MuaGVhZGVyKCJTdHJva2UgRW50cmllcyIsNSkKcGFuZG9jLnRhYmxlKHRhYmxlKHN0cm9rZXMkc3Ryb2tlKSkKCmBgYAojIyMgVXBkYXRlIGJpbmFyeSBjYXRlZ29yaWNhbCB2YXJpYWJsZXMgdG8gbnVtZXJpYwojIyMgcmVtb3ZlIGdlbmRlciB3aXRoIHZhbHVlIG9mICdvdGhlcicsIGNvZGUgIkZlbWFsZSBhcyAwLCBtYWxlIGFzIDEKYGBge3J9CnN0cm9rZXMgPC0gc3Ryb2tlcyAlPiUgZmlsdGVyKGdlbmRlciAhPSAiT3RoZXIiKQpzdHJva2VzJGdlbmRlciA8LSBpZmVsc2Uoc3Ryb2tlcyRnZW5kZXIgPT0gIkZlbWFsZSIsIDAsMSkKYGBgCgojIyMgY29udmVydCByZXNpZGVuY2UgdHlwZSB0byBiaW5hcnkgcmVzcG9uc2UuIDA9cnVyYWwsIDE9dXJiYW4KYGBge3J9CnN0cm9rZXMkUmVzaWRlbmNlX3R5cGUgPSBpZmVsc2Uoc3Ryb2tlcyRSZXNpZGVuY2VfdHlwZSA9PSAiUnVyYWwiLCAwLCAxKQpgYGAKCiMjIyBjb252ZXJ0IGV2ZXIgbWFycmllZCB0byBiaW5hcnkgcmVzcG9uc2UuIDA9Tm8sIDE9WWVzCmBgYHtyfQpzdHJva2VzJGV2ZXJfbWFycmllZCA8LSBpZmVsc2Uoc3Ryb2tlcyRldmVyX21hcnJpZWQgPT0gJ05vJywgMCwxKQpgYGAKCgojIyMgQ29udmVydCBzdHJva2UgdG8gJ3llcy9ubycgdmFsdWVzCgpgYGB7cn0Kc3Ryb2tlcyRzdHJva2UgPC0gaWZlbHNlKHN0cm9rZXMkc3Ryb2tlID09IDEsICJZZXMiLCAiTm8iKQpgYGAKCgojIyMgQ29udmVydCBtdWx0aS1jYXRlZ29yeSBjYXRlZ29yaWNhbCBmZWF0dXJlcyB0byBmYWN0b3JzCmBgYHtyfQojIHN0cm9rZXMkZ2VuZGVyIDwtIGFzLmZhY3RvcihzdHJva2VzJGdlbmRlcikKIyBzdHJva2VzJGh5cGVydGVuc2lvbiA8LSBhcy5mYWN0b3Ioc3Ryb2tlcyRoeXBlcnRlbnNpb24pCiMgc3Ryb2tlcyRoZWFydF9kaXNlYXNlIDwtIGFzLmZhY3RvcihzdHJva2VzJGhlYXJ0X2Rpc2Vhc2UpCiMgc3Ryb2tlcyRldmVyX21hcnJpZWQgPC0gYXMuZmFjdG9yKHN0cm9rZXMkZXZlcl9tYXJyaWVkKQpzdHJva2VzJHdvcmtfdHlwZSA8LSBhcy5mYWN0b3Ioc3Ryb2tlcyR3b3JrX3R5cGUpCiMgc3Ryb2tlcyRSZXNpZGVuY2VfdHlwZSA8LSBhcy5mYWN0b3Ioc3Ryb2tlcyRSZXNpZGVuY2VfdHlwZSkKc3Ryb2tlcyRzbW9raW5nX3N0YXR1cyA8LSBhcy5mYWN0b3Ioc3Ryb2tlcyRzbW9raW5nX3N0YXR1cykKc3Ryb2tlcyRzdHJva2UgPC0gYXMuZmFjdG9yKHN0cm9rZXMkc3Ryb2tlKQpzdHJva2VzJHNtb2tpbmdfc3RhdHVzIDwtIHJldmFsdWUoc3Ryb2tlcyRzbW9raW5nX3N0YXR1cywgYygiZm9ybWVybHkgc21va2VkIj0iZm9ybWVybHlfc21va2VkIiwgIm5ldmVyIHNtb2tlZCI9Im5ldmVyX3Ntb2tlZCIsICJzbW9rZXMiPSJzbW9rZXMiLCAiVW5rbm93biI9InVua25vd24iKSkKc3Ryb2tlcyR3b3JrX3R5cGUgPC0gcmV2YWx1ZShzdHJva2VzJHdvcmtfdHlwZSwgYygiU2VsZi1lbXBsb3llZCI9IlNlbGZfZW1wbG95ZWQiKSkKCmBgYAoKCiMjIyBDbGVhbiBibWkgZmVhdHVyZSAgIAoqIENvbnZlcnQgIk4vQSIgZW50cmllcyB0byBOQSB2YWx1ZXMKKiBDb252ZXJ0IGZyb20gY2hhcmFjdGVyIHRvIG51bWVyaWMgdmFsdWVzCmBgYHtyfQpzdHJva2VzIDwtIG5hX2lmKHN0cm9rZXMsIk4vQSIpCnN0cm9rZXMkYm1pIDwtIGFzLm51bWVyaWMoc3Ryb2tlcyRibWkpCmRpbShzdHJva2VzKQoKYGBgCiMjIERhdGEgRXhwbG9yYXRpb24KYGBge3IgZWNobz1GQUxTRSxyZXN1bHRzPSdhc2lzJ30KbWlzc2luZyA8LSBzYXBwbHkoc3Ryb2tlcywgZnVuY3Rpb24oeCkgc3VtKGlzLm5hKHgpKSkKcGFuZG9jLmhlYWRlcigiTWlzc2luZyBWYWx1ZXMiLCA0KQpwYW5kb2MudGFibGUobWlzc2luZykKYGBgCmBgYHtyfQpyZXF1aXJlKHRpZHl2ZXJzZSkKcmVxdWlyZShyY29tcGFuaW9uKQoKCiMgQ2FsY3VsYXRlIGEgcGFpcndpc2UgYXNzb2NpYXRpb24gYmV0d2VlbiBhbGwgdmFyaWFibGVzIGluIGEgZGF0YS1mcmFtZS4gSW4gcGFydGljdWxhciBub21pbmFsIHZzIG5vbWluYWwgd2l0aCBDaGktc3F1YXJlLCBudW1lcmljIHZzIG51bWVyaWMgd2l0aCBQZWFyc29uIGNvcnJlbGF0aW9uLCBhbmQgbm9taW5hbCB2cyBudW1lcmljIHdpdGggQU5PVkEuCiMgQWRvcHRlZCBmcm9tIGh0dHBzOi8vc3RhY2tvdmVyZmxvdy5jb20vYS81MjU1NzYzMS81OTA0MzcKbWl4ZWRfYXNzb2MgPSBmdW5jdGlvbihkZiwgY29yX21ldGhvZD0ic3BlYXJtYW4iLCBhZGp1c3RfY3JhbWVyc3ZfYmlhcz1UUlVFKXsKICAgIGRmX2NvbWIgPSBleHBhbmQuZ3JpZChuYW1lcyhkZiksIG5hbWVzKGRmKSwgIHN0cmluZ3NBc0ZhY3RvcnMgPSBGKSAlPiUgc2V0X25hbWVzKCJYMSIsICJYMiIpCgogICAgaXNfbm9taW5hbCA9IGZ1bmN0aW9uKHgpIGNsYXNzKHgpICVpbiUgYygiZmFjdG9yIiwgImNoYXJhY3RlciIpCiAgICAjIGh0dHBzOi8vY29tbXVuaXR5LnJzdHVkaW8uY29tL3Qvd2h5LWlzLXB1cnItaXMtbnVtZXJpYy1kZXByZWNhdGVkLzM1NTkKICAgICMgaHR0cHM6Ly9naXRodWIuY29tL3ItbGliL3JsYW5nL2lzc3Vlcy83ODEKICAgIGlzX251bWVyaWMgPC0gZnVuY3Rpb24oeCkgeyBpcy5pbnRlZ2VyKHgpIHx8IGlzX2RvdWJsZSh4KX0KCiAgICBmID0gZnVuY3Rpb24oeE5hbWUseU5hbWUpIHsKICAgICAgICB4ID0gIHB1bGwoZGYsIHhOYW1lKQogICAgICAgIHkgPSAgcHVsbChkZiwgeU5hbWUpCgogICAgICAgIHJlc3VsdCA9IGlmKGlzX25vbWluYWwoeCkgJiYgaXNfbm9taW5hbCh5KSl7CiAgICAgICAgICAgICMgdXNlIGJpYXMgY29ycmVjdGVkIGNyYW1lcnNWIGFzIGRlc2NyaWJlZCBpbiBodHRwczovL3JkcnIuaW8vY3Jhbi9yY29tcGFuaW9uL21hbi9jcmFtZXJWLmh0bWwKICAgICAgICAgICAgY3YgPSBjcmFtZXJWKGFzLmNoYXJhY3Rlcih4KSwgYXMuY2hhcmFjdGVyKHkpLCBiaWFzLmNvcnJlY3QgPSBhZGp1c3RfY3JhbWVyc3ZfYmlhcykKICAgICAgICAgICAgZGF0YS5mcmFtZSh4TmFtZSwgeU5hbWUsIGFzc29jPWN2LCB0eXBlPSJjcmFtZXJzViIpCgogICAgICAgIH1lbHNlIGlmKGlzX251bWVyaWMoeCkgJiYgaXNfbnVtZXJpYyh5KSl7CiAgICAgICAgICAgIGNvcnJlbGF0aW9uID0gY29yKHgsIHksIG1ldGhvZD1jb3JfbWV0aG9kLCB1c2U9ImNvbXBsZXRlLm9icyIpCiAgICAgICAgICAgIGRhdGEuZnJhbWUoeE5hbWUsIHlOYW1lLCBhc3NvYz1jb3JyZWxhdGlvbiwgdHlwZT0iY29ycmVsYXRpb24iKQoKICAgICAgICB9ZWxzZSBpZihpc19udW1lcmljKHgpICYmIGlzX25vbWluYWwoeSkpewogICAgICAgICAgICAjIGZyb20gaHR0cHM6Ly9zdGF0cy5zdGFja2V4Y2hhbmdlLmNvbS9xdWVzdGlvbnMvMTE5ODM1L2NvcnJlbGF0aW9uLWJldHdlZW4tYS1ub21pbmFsLWl2LWFuZC1hLWNvbnRpbnVvdXMtZHYtdmFyaWFibGUvMTI0NjE4IzEyNDYxOAogICAgICAgICAgICByX3NxdWFyZWQgPSBzdW1tYXJ5KGxtKHggfiB5KSkkci5zcXVhcmVkCiAgICAgICAgICAgIGRhdGEuZnJhbWUoeE5hbWUsIHlOYW1lLCBhc3NvYz1zcXJ0KHJfc3F1YXJlZCksIHR5cGU9ImFub3ZhIikKCiAgICAgICAgfWVsc2UgaWYoaXNfbm9taW5hbCh4KSAmJiBpc19udW1lcmljKHkpKXsKICAgICAgICAgICAgcl9zcXVhcmVkID0gc3VtbWFyeShsbSh5IH54KSkkci5zcXVhcmVkCiAgICAgICAgICAgIGRhdGEuZnJhbWUoeE5hbWUsIHlOYW1lLCBhc3NvYz1zcXJ0KHJfc3F1YXJlZCksIHR5cGU9ImFub3ZhIikKCiAgICAgICAgfWVsc2UgewogICAgICAgICAgICB3YXJuaW5nKHBhc3RlKCJ1bm1hdGNoZWQgY29sdW1uIHR5cGUgY29tYmluYXRpb246ICIsIGNsYXNzKHgpLCBjbGFzcyh5KSkpCiAgICAgICAgfQoKICAgICAgICAjIGZpbmFsbHkgYWRkIGNvbXBsZXRlIG9icyBudW1iZXIgYW5kIHJhdGlvIHRvIHRhYmxlCiAgICAgICAgcmVzdWx0ICU+JSBtdXRhdGUoY29tcGxldGVfb2JzX3BhaXJzPXN1bSghaXMubmEoeCkgJiAhaXMubmEoeSkpLCBjb21wbGV0ZV9vYnNfcmF0aW89Y29tcGxldGVfb2JzX3BhaXJzL2xlbmd0aCh4KSkgJT4lIHJlbmFtZSh4PXhOYW1lLCB5PXlOYW1lKQogICAgfQoKICAgICMgYXBwbHkgZnVuY3Rpb24gdG8gZWFjaCB2YXJpYWJsZSBjb21iaW5hdGlvbgogICAgbWFwMl9kZihkZl9jb21iJFgxLCBkZl9jb21iJFgyLCBmKQp9CmBgYAojIyMgQXNzb2NpYXRpb25zIGJldHdlZW4gZmVhdHVyZXMKYGBge3J9CmFzc29jaWF0aW9ucyA8LSBtaXhlZF9hc3NvYyhzdHJva2VzKQphc3NvY2lhdGlvbnMKYGBgCiMjIGJtaSBpbXB1dGF0aW9uCiMjIyMgYm1pIGhhcyBtdWx0aXBsZSBtaXNzaW5nIHZhbHVlcy4gSW5zdGVhZCBvZiB1c2luZyB0aGUgbWVhbiBvciBtZWRpYW4gb2YgdGhlIGVudGlyZSBkYXRhIHNldCB0byBpbXB1dGUgdGhlIG1pc3NpbmcgdmFsdWVzIGRldGVybWluZSBpZiBhIHNlY29uZCBmZWF0dXJlIGhhcyBhbiBhc3NvY2lhdGlvbiB3aXRoIGJtaQoKIyMjIGFzc29jaWF0aW9uIG9mIGJtaSB3aXRoIG90aGVyIHZhcmlhYmxlcwpgYGB7cn0KYm1pX2Fzc29jIDwtIGFzc29jaWF0aW9ucyAlPiUgZmlsdGVyKHkgPT0gImJtaSIpCmBgYAoKIyMjIFBsb3RzIG9mIGJtaSB2cyBvdGhlciBmZWF0dXJlcyAgIAoKYGBge3J9CiMgZ2dwbG90KGRhdGE9YXNzb2NpYXRpb25zLCBhZXMoeD14LCB5PXksIGZpbGw9YXNzb2MpKSArIGdlb21fdGlsZSgpICsgdGhlbWUoYXhpcy50ZXh0Lng9ZWxlbWVudF90ZXh0KGFuZ2xlPTkwLCB2anVzdD0wLjUsIGhqdXN0PTEpLCBheGlzLnRpdGxlID0gZWxlbWVudF9ibGFuaygpLCBwbG90LnRpdGxlPWVsZW1lbnRfdGV4dChoanVzdD0wLjUpKSArIGdndGl0bGUoIkFzc29jaWF0aW9uIEJldHdlZW4gRmVhdHVyZXMiKQpnZ3Bsb3QoZGF0YT1ibWlfYXNzb2MsIGFlcyh4PXgsIHk9eSwgZmlsbD1hc3NvYykpICsgZ2VvbV90aWxlKCkgKyB0aGVtZShheGlzLnRleHQueD1lbGVtZW50X3RleHQoYW5nbGU9OTAsIHZqdXN0PTAuNSwgaGp1c3Q9MSwgc2l6ZT0yMCksIGF4aXMudGV4dC55ID0gZWxlbWVudF90ZXh0KHNpemU9MjApLGF4aXMudGl0bGUgPSBlbGVtZW50X2JsYW5rKCksIHBsb3QudGl0bGU9ZWxlbWVudF90ZXh0KGhqdXN0PTAuNSkpICsgZ2d0aXRsZSgiQXNzb2NpYXRpb24gd2l0aCBibWkiKQpwbG90KGJtaSB+IC4sIGRhdGE9c3Ryb2tlcykKYGBgCgojIyMjIEFnZSBoYXMgYW4gYXNzb2NpYXRpb24gd2l0aCBibWksIHNvIHRoZSBkYXRhIHdpbGwgYmUgYmlubmVkIGJ5IGFnZSBhbmQgdGhlIG1pc3NpbmcgYm1pIHZhbHVlcyB3aWxsIGJlIGltcHV0ZWQgYmFzZWQgdXBvbiB0aGUgbWVkaWFuIHZhbHVlIG9mIHRoZSBiaW4gdGhleSBmYWxsIGluCgoKIyMjIENyZWF0ZSBiaW5zIGZvciBhZ2UgZmVhdHVyZSBhbmQgc2V0IG1pc3NpbmcgQk1JIHZhbHVlcyB0byBtZWRpYW4gb2YgdGhlIGJpbiB0aGV5IGZhbGwgaW4KYGBge3IgZWNobz1UUlVFfQpzdHJva2VzX29yaWdpbmFsIDwtIHN0cm9rZXMKc3Ryb2tlcyRhZ2VfY2F0IDwtIGJpbihzdHJva2VzJGFnZSwgbmJpbnM9MTAsIGxhYmVscz1jKCdhMScsICdhMicsICdhMycsICdhNCcsICdhNScsICdhNicsICdhNycsICdhOCcsJ2E5JywgJ2ExMCcpLCBtZXRob2QgPSAiY2x1c3RlcnMiKQoKc3Ryb2tlcyA8LSBzdHJva2VzICU+JSBncm91cF9ieShhZ2VfY2F0KSAlPiUgbXV0YXRlKGJtaS5uZXcgPSBtZWRpYW4oYm1pLCBuYS5ybT1UUlVFKSkKc3Ryb2tlcyRibWkgPC0gaWZlbHNlKGlzLm5hKHN0cm9rZXMkYm1pKSwgc3Ryb2tlcyRibWkubmV3LCBzdHJva2VzJGJtaSkKCiMgcmVtb3ZlIHdvcmtpbmcgY29sdW1ucwpzdHJva2VzIDwtIHN0cm9rZXNbLWMoMTIsMTMpXQpgYGAKCiMjIyBWZXJpZnkgbWlzc2luZyB2YWx1ZXMgYXJlIHJlc29sdmVkIGFuZCBkYXRhIHR5cGVzIGFyZSBhcyBleHBlY3RlZApgYGAge3IgZWNobz1GQUxTRSwgcmVzdWx0cz0nYXNpcyd9Cm1pc3NpbmcgPC0gc2FwcGx5KHN0cm9rZXMsIGZ1bmN0aW9uKHgpIHN1bShpcy5uYSh4KSkpCnBhbmRvYy5oZWFkZXIoIk1pc3NpbmcgVmFsdWVzIiwgNCkKcGFuZG9jLnRhYmxlKG1pc3NpbmcpCmBgYAoKYGBge3J9CgoKCnN0cihzdHJva2VzKQpgYGAKCiMjIyBFeHBsb3JlIHJlbGF0aW9uIG9mIGZlYXR1cmVzIHdpdGggcmVzcG9uc2UgdmFyaWFibGUgJ3N0cm9rZScKCgoKCmBgYHtyfQpwMSA8LSBnZ3Bsb3QoZGF0YT1zdHJva2VzLCBhZXMoc3Ryb2tlLCBmaWxsPWFzLmZhY3RvcihnZW5kZXIpKSkgKyBnZW9tX2Jhcihwb3NpdGlvbj0nZmlsbCcpICsgbGFicyhmaWxsPSJnZW5kZXIiKQpwMiA8LSBnZ3Bsb3QoZGF0YT1zdHJva2VzLCBhZXMoc3Ryb2tlLCBhZ2UpKSArIGdlb21fYm94cGxvdCgpCnAzIDwtIGdncGxvdChkYXRhPXN0cm9rZXMsIGFlcyhzdHJva2UsIGZpbGw9YXMuZmFjdG9yKGh5cGVydGVuc2lvbikpKSArIGdlb21fYmFyKHBvc2l0aW9uPSdmaWxsJykgKyBsYWJzKGZpbGw9Imh5cGVydGVuc2lvbiIpCnA0IDwtIGdncGxvdChkYXRhPXN0cm9rZXMsIGFlcyhzdHJva2UsIGZpbGw9YXMuZmFjdG9yKGhlYXJ0X2Rpc2Vhc2UpKSkgKyBnZW9tX2Jhcihwb3NpdGlvbj0nZmlsbCcpICsgbGFicyhmaWxsPSdoZWFydF9kaXNlYXNlJykKcDUgPC0gZ2dwbG90KGRhdGE9c3Ryb2tlcywgYWVzKHN0cm9rZSwgZmlsbD1hcy5mYWN0b3IoZXZlcl9tYXJyaWVkKSkpICsgZ2VvbV9iYXIocG9zaXRpb249J2ZpbGwnKSArIGxhYnMoZmlsbD0iZXZlcl9tYXJyaWVkIikKcDYgPC0gZ2dwbG90KGRhdGE9c3Ryb2tlcywgYWVzKHN0cm9rZSwgZmlsbD13b3JrX3R5cGUpKSArIGdlb21fYmFyKHBvc2l0aW9uPSdmaWxsJykKcDcgPC0gZ2dwbG90KGRhdGE9c3Ryb2tlcywgYWVzKHN0cm9rZSwgZmlsbD1hcy5mYWN0b3IoUmVzaWRlbmNlX3R5cGUpKSkgKyBnZW9tX2Jhcihwb3NpdGlvbj0nZmlsbCcpICsgbGFicyhmaWxsPSJSZXNpZGVuY2VfdHlwZSIpCnA4IDwtIGdncGxvdChkYXRhPXN0cm9rZXMsIGFlcyhzdHJva2UsIGF2Z19nbHVjb3NlX2xldmVsKSkgKyBnZW9tX2JveHBsb3QoKQpwOSA8LSBnZ3Bsb3QoZGF0YT1zdHJva2VzLCBhZXMoc3Ryb2tlLCBibWkpKSArIGdlb21fYm94cGxvdCgpCnAxMCA8LSBnZ3Bsb3QoZGF0YT1zdHJva2VzLCBhZXMoc3Ryb2tlLCBmaWxsPXNtb2tpbmdfc3RhdHVzKSkgKyBnZW9tX2Jhcihwb3NpdGlvbj0nZmlsbCcpCgpncmlkLmFycmFuZ2UocDEscDIscDMscDQsbnJvdz0yKQpncmlkLmFycmFuZ2UocDUscDYscDcscDgsbnJvdz0yKQpncmlkLmFycmFuZ2UocDkscDEwLG5yb3c9MSkKCmBgYApgYGB7ciBlY2hvPUZBTFNFLCByZXN1bHRzPSdhc2lzJ30KYWdlX3QgPC0gdC50ZXN0KHN0cm9rZXMkYWdlfnN0cm9rZXMkc3Ryb2tlLCBhbHRlcm5hdGl2ZT0ndHdvLnNpZGVkJykKZ2x1Y29zZV90IDwtIHQudGVzdChzdHJva2VzJGF2Z19nbHVjb3NlX2xldmVsfnN0cm9rZXMkc3Ryb2tlLCBhbHRlcm5hdGl2ZT0ndHdvLnNpZGVkJykKYm1pX3QgPC0gdC50ZXN0KHN0cm9rZXMkYm1pfnN0cm9rZXMkc3Ryb2tlLCBhbHRlcm5hdGl2ZT0ndHdvLnNpZGVkJykKCgpzbW9raW5nX2NoaTIgPC0gc3Ryb2tlcyAlPiUgc2VsZWN0KHNtb2tpbmdfc3RhdHVzLHN0cm9rZSkgJT4lIHRhYmxlKCkgJT4lIGNoaXNxLnRlc3QoKQpnZW5kZXJfY2hpMiA8LSBzdHJva2VzICU+JSBzZWxlY3QoZ2VuZGVyLHN0cm9rZSkgJT4lIHRhYmxlKCkgJT4lIGNoaXNxLnRlc3QoKQpoeXBlcnRlbnNpb25fY2hpMiA8LSBzdHJva2VzICU+JSBzZWxlY3QoaHlwZXJ0ZW5zaW9uLHN0cm9rZSkgJT4lIHRhYmxlKCkgJT4lIGNoaXNxLnRlc3QoKQpoZF9jaGkyIDwtIHN0cm9rZXMgJT4lIHNlbGVjdChoZWFydF9kaXNlYXNlLHN0cm9rZSkgJT4lIHRhYmxlKCkgJT4lIGNoaXNxLnRlc3QoKQplbV9jaGkyIDwtIHN0cm9rZXMgJT4lIHNlbGVjdChldmVyX21hcnJpZWQsc3Ryb2tlKSAlPiUgdGFibGUoKSAlPiUgY2hpc3EudGVzdCgpCnd0X2NoaTIgPC0gc3Ryb2tlcyAlPiUgc2VsZWN0KHdvcmtfdHlwZSxzdHJva2UpICU+JSB0YWJsZSgpICU+JSBjaGlzcS50ZXN0KCkKcnRfY2hpMiA8LSBzdHJva2VzICU+JSBzZWxlY3QoUmVzaWRlbmNlX3R5cGUsc3Ryb2tlKSAlPiUgdGFibGUoKSAlPiUgY2hpc3EudGVzdCgpCgphZ2VwIDwtIGFnZV90JHAudmFsdWUKZ2x1Y29zcCA8LSBnbHVjb3NlX3QkcC52YWx1ZQpibWlwIDwtIGJtaV90JHAudmFsdWUKCnNtb2tpbmdwIDwtIHNtb2tpbmdfY2hpMiRwLnZhbHVlCmdlbmRlcnAgPC0gZ2VuZGVyX2NoaTIkcC52YWx1ZQpoeXBlcnRlbnNpb25wIDwtIGh5cGVydGVuc2lvbl9jaGkyJHAudmFsdWUKaGRwIDwtIGhkX2NoaTIkcC52YWx1ZQplbXAgPC0gZW1fY2hpMiRwLnZhbHVlCnd0cCA8LSB3dF9jaGkyJHAudmFsdWUKcnRwIDwtIHJ0X2NoaTIkcC52YWx1ZQoKdHRlc3RfdmFsdWVzIDwtIGMoYWdlcCwgZ2x1Y29zcCwgYm1pcCkKdHRlc3RfbGFiZWxzIDwtIGMoImFnZSIsICJnbHVjb3NlIiwgImJtaSIpCgpjaGlzcV92YWx1ZXMgPC0gYyhzbW9raW5ncCwgZ2VuZGVycCwgaHlwZXJ0ZW5zaW9ucCwgaGRwLCBlbXAsIHd0cCwgcnRwKQpjaGlzcV9sYWJlbHMgPC0gYygic21va2luZ19zdGF0dXMiLCAiZ2VuZGVyIiwgImh5cGVydGVuc2lvbiIsICJoZWFydF9kaXNlYXNlIiwgImV2ZXJfbWFycmllZCIsICJ3b3JrX3R5cGUiLCAicmVzaWRlbmNlX3R5cGUiKQoKdGRmIDwtIGRhdGEuZnJhbWUodHRlc3RfbGFiZWxzLCB0dGVzdF92YWx1ZXMpCmNkZiA8LSBkYXRhLmZyYW1lKGNoaXNxX2xhYmVscywgY2hpc3FfdmFsdWVzKQoKCgpgYGAKYGBge3IgZWNobz1GQUxTRSwgcmVzdWx0cz0nYXNpcyd9CnBhbmRvYy5oZWFkZXIoIkNoaS1TcXVhcmUgVGVzdHMiLCAzKQpwYW5kb2MudGFibGUoY2RmKQoKcGFuZG9jLmhlYWRlcigiVC1UZXN0cyIsMykKcGFuZG9jLnRhYmxlKHRkZikKYGBgCgoKCiMjIyMgVGhlIGZlYXR1cmVzIGdlbmRlciBhbmQgcmVzaWRlbmNlIHR5cGUgZG8gbm90IHNob3cgYSBzdGF0aXN0aWNhbGx5IHNpZ25pZmljYW50IGFzc29jaWF0aW9uIHdpdGggdGhlIHN0cm9rZSBvdXRjb21lLiBUaGlzIGNhbiBiZSBvYnNlcnZlZCB2aXN1YWxseSBhcyB3ZWxsIGFzIGJ5IHRoZSBDaGktU3F1YXJlIHRlc3RzLgoKIyMjIFJldmlldyBkaXN0cmlidXRpb24gb2YgbnVtZXJpYyBmZWF0dXJlcwoKYGBge3J9Cmhpc3Qoc3Ryb2tlcyRibWksIG1haW49ImJtaSIsIHhsYWI9ImJtaSIpCmhpc3Qoc3Ryb2tlcyRhdmdfZ2x1Y29zZV9sZXZlbCwgbWFpbj0iYXZlcmFnZSBnbHVjb3NlIGxldmVsIiwgeGxhYj0iYXZnX2dsdWNvc2VfbGV2ZWwiKQpoaXN0KHN0cm9rZXMkYWdlLCBtYWluPSJhZ2UiLCB4bGFiPSJhZ2UiKQoKCmBgYAoKIyMjIyBCb3RoIGF2Z19nbHVjb3NlX2xldmVsIGFuZCBibWkgYXJlIHJpZ2h0LXNrZXdlZC4gVG8gaGVscCBjb3JyZWN0IHRoaXMgYm90aCB2YXJpYWJsZXMgd2lsbCBiZSByZXBsYWNlZCB3aXRoIHRoZWlyIHJlc3BlY3RpdmUgbG9nIHZhbHVlcwpgYGB7cn0Kc3Ryb2tlcyRibWkgPC0gbG9nKHN0cm9rZXMkYm1pKQpzdHJva2VzJGF2Z19nbHVjb3NlX2xldmVsIDwtIGxvZyhzdHJva2VzJGF2Z19nbHVjb3NlX2xldmVsKQoKaGlzdChzdHJva2VzJGJtaSkKaGlzdChzdHJva2VzJGF2Z19nbHVjb3NlX2xldmVsKQpgYGAKIyMjIyBTaW5jZSBuZWl0aGVyIGdlbmRlciBub3IgcmVzaWRlbmNlX3R5cGUgc2hvdyBhbnkgc3RhdGlzdGljYWxseSBzaWduaWZpY2FudCBjb3JyZWxhdGlvbiB3aXRoIHRoZSBzdHJva2Ugb3V0Y29tZSBib3RoIGZlYXR1cmVzIHdpbGwgYmUgcmVtb3ZlZCB0byBhdm9pZCBhbnkgdW5pbnRlbnRpb25hbCBtb2RsZSBjb21wbGV4aXR5IG9yIG5vaXNlCiAKYGBge3J9CnN0cm9rZXMgPC0gc3Ryb2tlc1stYygxLDcpXQpgYGAKIAogCiAKIAoKIyMgUHJlcGFyZSBEYXRhIFNldHMgZm9yIE1vZGVsIFVzYWdlCiogU3BsaXQgaW50byB0cmFpbiwgdmFsaWRhdGlvbiBhbmQgdGVzdCBkYXRhc2V0cyAgIAoqIENyZWF0ZSBiYWxhbmNlZCB0cmFpbmluZyBkYXRhIHNldHMgICAKKiBTZXBhcmF0ZSBpbnRvIGZlYXR1cmVzIGFuZCBsYWJlbHMKKiBTY2FsZSBEYXRhc2V0cyAgCiogT25lLUhvdCBFbmNvZGUgRGF0YXNldHMKYGBge3IgZWNobz1UUlVFLCByZXN1bHRzID0gJ2hpZGUnfQoKIyBjcmVhdGUgdHJhaW4vdGVzdCBkYXRhc2V0IGFzIHdlbGwgYXMgc3Vic2V0IG9mIHRyYWluIGZvciBjcm9zcy12YWxpZGF0aW9uIGFuZCBldmFsdWF0aW9uIHVzYWdlCnNldC5zZWVkKDEyMykKc2VsZWN0aW9uIDwtIGNyZWF0ZURhdGFQYXJ0aXRpb24oc3Ryb2tlcyRzdHJva2UsIHA9MC44LCBsaXN0PUZBTFNFKSAgIAp0cmFpbkRhdGFGdWxsID0gc3Ryb2tlc1tzZWxlY3Rpb24sXSAgICAKdGVzdERhdGEgPSBzdHJva2VzWy1zZWxlY3Rpb24sXQpzZXQuc2VlZCgxMjMpCnNlbGVjdGlvbjIgPC0gY3JlYXRlRGF0YVBhcnRpdGlvbih0cmFpbkRhdGFGdWxsJHN0cm9rZSwgcD0wLjgsIGxpc3Q9RkFMU0UpCnRyYWluRGF0YSA9IHRyYWluRGF0YUZ1bGxbc2VsZWN0aW9uMixdCnZhbERhdGEgPSB0cmFpbkRhdGFGdWxsWy1zZWxlY3Rpb24yLF0KCiNjcmVhdGUgYmFsYW5jZWQgdHJhaW5pbmcgZGF0YXNldHMKCnRyYWluRGF0YUJhbGFuY2VkIDwtIFJPU0Uoc3Ryb2tlIH4gLiwgZGF0YT10cmFpbkRhdGEsIHA9MC41KSRkYXRhCnRyYWluRGF0YUZ1bGxCYWxhbmNlZCA8LSBST1NFKHN0cm9rZSB+IC4sIGRhdGE9dHJhaW5EYXRhRnVsbCwgcD0wLjUpJGRhdGEKCiMgc2VwYXJhdGUgZmVhdHVyZXMgYW5kIGxhYmVscwoKdmFsRGF0YUZlYXR1cmVzIDwtIHZhbERhdGFbLTldCnZhbERhdGFMYWJlbHMgPC0gdmFsRGF0YVs5XQoKdGVzdERhdGFGZWF0dXJlcyA8LSB0ZXN0RGF0YVstOV0KdGVzdERhdGFMYWJlbHMgPC0gdGVzdERhdGFbOV0KCnRyYWluRGF0YUZlYXR1cmVzIDwtIHRyYWluRGF0YVstOV0KdHJhaW5EYXRhTGFiZWxzIDwtIHRyYWluRGF0YVs5XQoKdHJhaW5EYXRhRnVsbEZlYXR1cmVzIDwtIHRyYWluRGF0YUZ1bGxbLTldCnRyYWluRGF0YUZ1bGxMYWJlbHMgPC0gdHJhaW5EYXRhRnVsbFs5XQoKdHJhaW5EYXRhQmFsYW5jZWRGZWF0dXJlcyA8LSB0cmFpbkRhdGFCYWxhbmNlZFstOV0KdHJhaW5EYXRhQmFsYW5jZWRMYWJlbHMgPC0gdHJhaW5EYXRhQmFsYW5jZWRbOV0KCnRyYWluRGF0YUZ1bGxCYWxhbmNlZEZlYXR1cmVzIDwtIHRyYWluRGF0YUZ1bGxCYWxhbmNlZFstOV0KdHJhaW5EYXRhRnVsbEJhbGFuY2VkTGFiZWxzIDwtIHRyYWluRGF0YUZ1bGxCYWxhbmNlZFs5XQoKIyBzY2FsZSBmZWF0dXJlcyBkYXRhIHNldHMuIHRlc3REYXRhIHNjYWxlcyB3aXRoIHRyYWluRGF0YUZ1bGwgd2hpbGUgdmFsRGF0YSBzY2FsZXMgd2l0aCB0cmFpbkRhdGEKCnByZVByb2NTY2FsZUZ1bGxUcmFpbiA8LSBwcmVQcm9jZXNzKHRyYWluRGF0YUZ1bGxCYWxhbmNlZEZlYXR1cmVzLCBtZXRob2Q9YygiY2VudGVyIiwgInNjYWxlIikpCnByZVByb2NTY2FsZVRyYWluIDwtIHByZVByb2Nlc3ModHJhaW5EYXRhQmFsYW5jZWRGZWF0dXJlcywgbWV0aG9kPWMoImNlbnRlciIsICJzY2FsZSIpKQoKdHJhaW5EYXRhRnVsbEJhbGFuY2VkRmVhdHVyZXNfcyA8LSBwcmVkaWN0KHByZVByb2NTY2FsZUZ1bGxUcmFpbiwgdHJhaW5EYXRhRnVsbEJhbGFuY2VkRmVhdHVyZXMpCnRlc3REYXRhRmVhdHVyZXNfcyA8LSBwcmVkaWN0KHByZVByb2NTY2FsZUZ1bGxUcmFpbiwgdGVzdERhdGFGZWF0dXJlcykKCnRyYWluRGF0YUJhbGFuY2VkRmVhdHVyZXNfcyA8LSBwcmVkaWN0KHByZVByb2NTY2FsZVRyYWluLCB0cmFpbkRhdGFCYWxhbmNlZEZlYXR1cmVzKQp2YWxEYXRhRmVhdHVyZXNfcyA8LSBwcmVkaWN0KHByZVByb2NTY2FsZVRyYWluLCB2YWxEYXRhRmVhdHVyZXMpCgojIG9uZS1ob3QgZW5jb2RlICBkYXRhc2V0cwpkdW1teTEgPC0gZHVtbXlWYXJzKCJ+IC4iLCBkYXRhPXRyYWluRGF0YUZ1bGxCYWxhbmNlZEZlYXR1cmVzX3MpCnRyYWluRGF0YUZ1bGxCYWxhbmNlZEZlYXR1cmVzX3NlIDwtIGRhdGEuZnJhbWUocHJlZGljdChkdW1teTEsbmV3ZGF0YT10cmFpbkRhdGFGdWxsQmFsYW5jZWRGZWF0dXJlc19zKSkKCmR1bW15MiA8LSBkdW1teVZhcnMoIn4gLiIsIGRhdGE9dHJhaW5EYXRhQmFsYW5jZWRGZWF0dXJlcykKdHJhaW5EYXRhQmFsYW5jZWRGZWF0dXJlc19lIDwtIGRhdGEuZnJhbWUocHJlZGljdChkdW1teTIsbmV3ZGF0YT10cmFpbkRhdGFCYWxhbmNlZEZlYXR1cmVzKSkKCmR1bW15MyA8LSBkdW1teVZhcnMoIn4gLiIsIGRhdGE9dHJhaW5EYXRhRmVhdHVyZXMpCnRyYWluRGF0YUZlYXR1cmVzX2UgPC0gZGF0YS5mcmFtZShwcmVkaWN0KGR1bW15MyxuZXdkYXRhPXRyYWluRGF0YUZlYXR1cmVzKSkKCmR1bW15NCA8LSBkdW1teVZhcnMoIn4gLiIsIGRhdGE9dHJhaW5EYXRhRnVsbEZlYXR1cmVzKQp0cmFpbkRhdGFGdWxsRmVhdHVyZXNfZSA8LSBkYXRhLmZyYW1lKHByZWRpY3QoZHVtbXk0LG5ld2RhdGE9dHJhaW5EYXRhRnVsbEZlYXR1cmVzKSkKCgp0ZXN0RGF0YUZlYXR1cmVzX3NlIDwtIGRhdGEuZnJhbWUocHJlZGljdChkdW1teTEsbmV3ZGF0YT10ZXN0RGF0YUZlYXR1cmVzX3MpKQoKCnZhbERhdGFGZWF0dXJlc19lIDwtIGRhdGEuZnJhbWUocHJlZGljdChkdW1teTIsbmV3ZGF0YT12YWxEYXRhRmVhdHVyZXMpKQoKZnVsbHNpemUgPC0gZGltKHRyYWluRGF0YUZ1bGwpCnRyYWluc2l6ZSA8LSBkaW0odHJhaW5EYXRhKQp2YWxzaXplIDwtIGRpbSh2YWxEYXRhKQp0ZXN0c2l6ZSA8LSBkaW0odGVzdERhdGEpCgpkaXN0ZnVsbCA8LSB0YWJsZSh0cmFpbkRhdGFGdWxsJHN0cm9rZSkKZGlzdHRyYWluIDwtIHRhYmxlKHRyYWluRGF0YSRzdHJva2UpCmRpc3R2YWwgPC0gdGFibGUodmFsRGF0YSRzdHJva2UpCmRpc3R0ZXN0IDwtIHRhYmxlKHRlc3REYXRhJHN0cm9rZSkKCgpgYGAKIyMjIFZlcmlmeSBkaW1lbnNpb25zIGFuZCBkaXN0cmlidXRpb24gb2YgcmVzcG9uc2UgdmFyaWFibGUgZm9yIGRhdGFzZXRzICAgCgpgYGB7ciBlY2hvPUZBTFNFLCByZXN1bHRzPSdhc2lzJ30KcGFuZG9jLmhlYWRlcigidHJhaW5EYXRhRnVsbCIsIDQpCnBhbmRvYy50YWJsZShmdWxsc2l6ZSkKCnBhbmRvYy5oZWFkZXIoInRyYWluRGF0YSIsIDQpCnBhbmRvYy50YWJsZSh0cmFpbnNpemUpCgpwYW5kb2MuaGVhZGVyKCJ2YWxEYXRhIiwgNCkKcGFuZG9jLnRhYmxlKHZhbHNpemUpCgpwYW5kb2MuaGVhZGVyKCJ0ZXN0RGF0YSIsNCkKcGFuZG9jLnRhYmxlKHRlc3RzaXplKQoKcGFuZG9jLmhlYWRlcigidHJhaW5EYXRhRnVsbCIsIDQpCnBhbmRvYy50YWJsZShwcm9wLnRhYmxlKGRpc3RmdWxsKSkKCnBhbmRvYy5oZWFkZXIoInRyYWluRGF0YSIsIDQpCnBhbmRvYy50YWJsZShwcm9wLnRhYmxlKGRpc3R0cmFpbikpCgpwYW5kb2MuaGVhZGVyKCJ2YWxEYXRhIiwgNCkKcGFuZG9jLnRhYmxlKHByb3AudGFibGUoZGlzdHZhbCkpCgpwYW5kb2MuaGVhZGVyKCJ0ZXN0RGF0YSIsNCkKcGFuZG9jLnRhYmxlKHByb3AudGFibGUoZGlzdHRlc3QpKQoKCgpgYGAKCiMjIyMgVGhlIHByb3BvcnRpb24gb2YgIm5vIiB0byAieWVzIiB2YWx1ZXMgaW4gYWxsIGRhdGFzZXRzIGlzIGVxdWl2YWxlbnQuCgoKCgoKCgojIEZpdCBNb2RlbHMKIyMgRGVmaW5lIGNvbnRyb2wgZm9yIG1vZGVscyB0cmFpbmVkIHdpdGggQ2FyZXQKYGBge3J9Cm15Q29udHJvbCA8LSB0cmFpbkNvbnRyb2wobWV0aG9kPSdyZXBlYXRlZGN2JywKICAgICAgICAgICAgICAgICAgICAgICAgICBudW1iZXI9MTAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwZWF0cz01LAogICAgICAgICAgICAgICAgICAgICAgICAgIHZlcmJvc2VJdGVyID0gRkFMU0UsCiAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3NQcm9icz1UUlVFLAogICAgICAgICAgICAgICAgICAgICAgICAgIHN1bW1hcnlGdW5jdGlvbj10d29DbGFzc1N1bW1hcnksCiAgICAgICAgICAgICAgICAgICAgICAgICAgc2VsZWN0aW9uRnVuY3Rpb249Im9uZVNFIiwKICAgICAgICAgICAgICAgICAgICAgICAgICBzYW1wbGluZz0ncm9zZScpCgpteUNvbnRyb2wyIDwtIHRyYWluQ29udHJvbChtZXRob2Q9J3JlcGVhdGVkY3YnLAogICAgICAgICAgICAgICAgICAgICAgICAgIG51bWJlcj0xMCwKICAgICAgICAgICAgICAgICAgICAgICAgICByZXBlYXRzPTUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgdmVyYm9zZUl0ZXIgPSBGQUxTRSwKICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzc1Byb2JzPVRSVUUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgc3VtbWFyeUZ1bmN0aW9uPXR3b0NsYXNzU3VtbWFyeSwKICAgICAgICAgICAgICAgICAgICAgICAgICBzZWxlY3Rpb25GdW5jdGlvbj0ib25lU0UiKQpgYGAKCgoKIyMgS05OIE1vZGVsIFRyYWluaW5nIGFuZCBFdmFsdWF0aW9uCkVuc3VyaW5nIHRoZSBzcXVhcmUgcm9vdCBvZiB0aGUgbnVtYmVyIG9mIG9ic2VydmF0aW9ucyBpcyBjb250YWluZWQgaW4gdGhlIHZhbHVlcyBvZiBrIGJlaW5nIGV4cGxvcmVkCmBgYHtyIHdhcm5pbmc9RkFMU0V9CnNldC5zZWVkKDEyMykKCmtubl9ncmlkIDwtIGV4cGFuZC5ncmlkKGs9c2VxKDEsMTAwLGJ5PTIpKQoKa25uX3RyYWluIDwtIGNiaW5kKHRyYWluRGF0YUZlYXR1cmVzX2UsIHRyYWluRGF0YUxhYmVscykKa25uX21vZGVsIDwtIGNhcmV0Ojp0cmFpbihzdHJva2UgfiAuLCBkYXRhPWtubl90cmFpbiwgbWV0aG9kPSdrbm4nLCB0ckNvbnRyb2w9bXlDb250cm9sLCB0dW5lR3JpZCA9IGtubl9ncmlkLCBwcmVQcm9jID0gJ3JhbmdlJywgbWV0cmljPSdST0MnKQoKCgpgYGAKCgoKCmBgYHtyfQoKCnBsb3Qoa25uX21vZGVsKQoKa25uX3ByZWQgPC0gcHJlZGljdChrbm5fbW9kZWwsIG5ld2RhdGE9dmFsRGF0YUZlYXR1cmVzX2UpCmtubl9wcm9iIDwtIHByZWRpY3Qoa25uX21vZGVsLCBuZXdkYXRhPXZhbERhdGFGZWF0dXJlc19lLCB0eXBlPSdwcm9iJykKa25uX3Jlc3VsdHMgPC0gY29uZnVzaW9uTWF0cml4KGtubl9wcmVkLCB2YWxEYXRhTGFiZWxzJHN0cm9rZSwgcG9zaXRpdmUgPSAiWWVzIikKCmtubl9yZXN1bHRzMiA8LSBkYXRhLmZyYW1lKGFzLmZhY3Rvcihrbm5fcHJlZCksIGFzLmZhY3Rvcih2YWxEYXRhTGFiZWxzJHN0cm9rZSksIGtubl9wcm9iKQpjb2xuYW1lcyhrbm5fcmVzdWx0czIpIDwtIGMoInByZWQiLCAib2JzIiwgIk5vIiwgIlllcyIpCmtubl9tb2RlbAprbm5fcmVzdWx0cwprbm5fMmNsYXNzIDwtIHR3b0NsYXNzU3VtbWFyeShrbm5fcmVzdWx0czIsIGxldiA9IGMoIk5vIiwgIlllcyIpKQprbm5fMmNsYXNzWzFdCmtubl9yb2MgPC0gcm9jKHZhbERhdGFMYWJlbHMkc3Ryb2tlIH4ga25uX3Byb2IkWWVzLCBwbG90PVRSVUUsIHByaW50LmF1Yz1UUlVFLCBjb2w9J2JsYWNrJywgbHdkPTQsIGxlZ2FjeS5heGVzPVRSVUUsIG1haW49IlJPQyBDdXJ2ZSIsIGFkZD1GQUxTRSkKYGBgCgoKCgojIyBMb2dpc3RpYyBSZWdyZXNzaW9uIE1vZGVsIHdpdGggRWxhc3RpYyBOZXQgUmVndWxhcml6YXRpb24KYGBge3J9CnNldC5zZWVkKDEyMykKCmdsbV9ncmlkIDwtIGV4cGFuZC5ncmlkKGFscGhhPXNlcSgwLDEsIGxlbmd0aD0xMCksIGxhbWJkYSA9IDEwXnNlcSgtMywzLCBsZW5ndGg9MTAwKSkKCmdsbV90cmFpbiA8LSBjYmluZCh0cmFpbkRhdGFGZWF0dXJlc19lLCB0cmFpbkRhdGFMYWJlbHMpCmdsbV9tb2RlbCA8LSBjYXJldDo6dHJhaW4oc3Ryb2tlIH4gLiwgZGF0YT1nbG1fdHJhaW4sIG1ldGhvZD0nZ2xtbmV0JywgdHJDb250cm9sPW15Q29udHJvbCwgdHVuZUdyaWQgPSBnbG1fZ3JpZCwgcHJlUHJvYyA9ICJyYW5nZSIsIG1ldHJpYz0nUk9DJywgZmFtaWx5PSdiaW5vbWlhbCcpCmBgYAoKYGBgIHtSfQoKCmdsbV9wcmVkIDwtIHByZWRpY3QoZ2xtX21vZGVsLCBuZXdkYXRhID0gdmFsRGF0YUZlYXR1cmVzX2UpCmdsbV9wcm9iIDwtIHByZWRpY3QoZ2xtX21vZGVsLCBuZXdkYXRhID0gdmFsRGF0YUZlYXR1cmVzX2UsIHR5cGU9J3Byb2InKQpnbG1fcmVzdWx0cyA8LSBjb25mdXNpb25NYXRyaXgoZ2xtX3ByZWQsIHZhbERhdGFMYWJlbHMkc3Ryb2tlLCBwb3NpdGl2ZT0iWWVzIikKCmdsbV9yZXN1bHRzCgoKZ2xtX3Jlc3VsdHMyIDwtIGRhdGEuZnJhbWUoYXMuZmFjdG9yKGdsbV9wcmVkKSwgYXMuZmFjdG9yKHZhbERhdGFMYWJlbHMkc3Ryb2tlKSwgZ2xtX3Byb2IpCgoKY29sbmFtZXMoZ2xtX3Jlc3VsdHMyKSA8LSBjKCJwcmVkIiwgIm9icyIsICJObyIsICJZZXMiKQoKCgpnbG1fMmNsYXNzIDwtIHR3b0NsYXNzU3VtbWFyeShnbG1fcmVzdWx0czIsIGxldj1jKCJObyIsICJZZXMiKSkKZ2xtXzJjbGFzc1sxXQpnbG1fcm9jIDwtIHJvYyh2YWxEYXRhTGFiZWxzJHN0cm9rZSB+IGdsbV9wcm9iJFllcywgcGxvdD1UUlVFLCBwcmludC5hdWM9VFJVRSwgY29sPSdibGFjaycsIGx3ZD00LCBsZWdhY3kuYXhlcz1UUlVFLCBtYWluPSJST0MgQ3VydmUiLCBhZGQ9RkFMU0UpCmNvZWYoZ2xtX21vZGVsJGZpbmFsTW9kZWwsZ2xtX21vZGVsJGJlc3RUdW5lJGxhbWJkYSkKdmFySW1wKGdsbV9tb2RlbCkKYGBgCgoKCiMjIERlY2lzaW9uIFRyZWUgTW9kZWwKCmBgYHtyfQoKCnNldC5zZWVkKDEyMykKCnRyZWVfZ3JpZCA8LSBleHBhbmQuZ3JpZChjcD1zZXEoMC4wLDEuMCxieT0uMDAxKSkKCgoKdHJlZV9kYXRhIDwtIGNiaW5kKHRyYWluRGF0YUZlYXR1cmVzLCB0cmFpbkRhdGFMYWJlbHMpCnRyZWVfbW9kZWwgPC0gY2FyZXQ6OnRyYWluKHN0cm9rZSB+LixkYXRhPXRyZWVfZGF0YSwgbWV0aG9kPSdycGFydCcsIHRyQ29udHJvbD1teUNvbnRyb2wsIHByZVByb2MgPSAicmFuZ2UiLCBtZXRyaWM9J1JPQycsIHR1bmVHcmlkPXRyZWVfZ3JpZCkKCmBgYAoKYGBge3J9CgoKCgp0cmVlX3ByZWQgPC0gcHJlZGljdCh0cmVlX21vZGVsLCB2YWxEYXRhRmVhdHVyZXMpCnRyZWVfcHJvYiA8LSBwcmVkaWN0KHRyZWVfbW9kZWwsIG5ld2RhdGEgPSB2YWxEYXRhRmVhdHVyZXMsIHR5cGU9J3Byb2InKQp0cmVlX3Jlc3VsdHMgPC0gY29uZnVzaW9uTWF0cml4KHRyZWVfcHJlZCwgdmFsRGF0YUxhYmVscyRzdHJva2UsIHBvc2l0aXZlPSJZZXMiKQp0cmVlX3Jlc3VsdHMKCgp0cmVlX3Jlc3VsdHMyIDwtIGRhdGEuZnJhbWUoYXMuZmFjdG9yKHRyZWVfcHJlZCksIGFzLmZhY3Rvcih2YWxEYXRhTGFiZWxzJHN0cm9rZSksIHRyZWVfcHJvYikKCgpjb2xuYW1lcyh0cmVlX3Jlc3VsdHMyKSA8LSBjKCJwcmVkIiwgIm9icyIsICJObyIsICJZZXMiKQoKCgp0cmVlXzJjbGFzcyA8LSB0d29DbGFzc1N1bW1hcnkodHJlZV9yZXN1bHRzMiwgbGV2PWMoIk5vIiwgIlllcyIpKQp0cmVlXzJjbGFzc1sxXQp0cmVlX3JvYyA8LSByb2ModmFsRGF0YUxhYmVscyRzdHJva2UgfiB0cmVlX3Byb2IkWWVzLCBwbG90PVRSVUUsIHByaW50LmF1Yz1UUlVFLCBjb2w9J2JsYWNrJywgbHdkPTQsIGxlZ2FjeS5heGVzPVRSVUUsIG1haW49IlJPQyBDdXJ2ZSIsIGFkZD1GQUxTRSkKZmFuY3lScGFydFBsb3QodHJlZV9tb2RlbCRmaW5hbE1vZGVsKQpgYGAKQ29uc2lkZXJpbmcgdGhlIHBvb3IgcGVyZm9ybWFuY2Ugb2YgdGhlIGRlY2lzaW9uIHRyZWUsIGEgc2Vjb25kIG1vZGVsIHdpbGwgYmUgYnVpbHQgYnV0IHdpbGwgbm90IGJhbGFuY2UgdGhlIGRhdGFzZXQgcHJpb3IgdG8gdXNlCgoKYGBge3J9CgoKc2V0LnNlZWQoMTIzKQoKdHJlZV9ncmlkIDwtIGV4cGFuZC5ncmlkKGNwPXNlcSgwLjAsMS4wLGJ5PS4wMDEpKQoKCgp0cmVlX2RhdGEgPC0gY2JpbmQodHJhaW5EYXRhRmVhdHVyZXMsIHRyYWluRGF0YUxhYmVscykKdHJlZV9tb2RlbCA8LSBjYXJldDo6dHJhaW4oc3Ryb2tlIH4uLGRhdGE9dHJlZV9kYXRhLCBtZXRob2Q9J3JwYXJ0JywgdHJDb250cm9sPW15Q29udHJvbDIsIHByZVByb2MgPSAicmFuZ2UiLCBtZXRyaWM9J1JPQycsIHR1bmVHcmlkPXRyZWVfZ3JpZCkKCmBgYAoKYGBge3J9CgoKCgp0cmVlX3ByZWQgPC0gcHJlZGljdCh0cmVlX21vZGVsLCB2YWxEYXRhRmVhdHVyZXMpCnRyZWVfcHJvYiA8LSBwcmVkaWN0KHRyZWVfbW9kZWwsIG5ld2RhdGEgPSB2YWxEYXRhRmVhdHVyZXMsIHR5cGU9J3Byb2InKQp0cmVlX3Jlc3VsdHMgPC0gY29uZnVzaW9uTWF0cml4KHRyZWVfcHJlZCwgdmFsRGF0YUxhYmVscyRzdHJva2UsIHBvc2l0aXZlPSJZZXMiKQp0cmVlX3Jlc3VsdHMKCgp0cmVlX3Jlc3VsdHMyIDwtIGRhdGEuZnJhbWUoYXMuZmFjdG9yKHRyZWVfcHJlZCksIGFzLmZhY3Rvcih2YWxEYXRhTGFiZWxzJHN0cm9rZSksIHRyZWVfcHJvYikKCgpjb2xuYW1lcyh0cmVlX3Jlc3VsdHMyKSA8LSBjKCJwcmVkIiwgIm9icyIsICJObyIsICJZZXMiKQoKCgp0cmVlXzJjbGFzcyA8LSB0d29DbGFzc1N1bW1hcnkodHJlZV9yZXN1bHRzMiwgbGV2PWMoIk5vIiwgIlllcyIpKQp0cmVlXzJjbGFzc1sxXQp0cmVlX3JvYyA8LSByb2ModmFsRGF0YUxhYmVscyRzdHJva2UgfiB0cmVlX3Byb2IkWWVzLCBwbG90PVRSVUUsIHByaW50LmF1Yz1UUlVFLCBjb2w9J2JsYWNrJywgbHdkPTQsIGxlZ2FjeS5heGVzPVRSVUUsIG1haW49IlJPQyBDdXJ2ZSIsIGFkZD1GQUxTRSkKZmFuY3lScGFydFBsb3QodHJlZV9tb2RlbCRmaW5hbE1vZGVsKQpgYGAKCiMjIFJhbmRvbSBGb3Jlc3QgTW9kZWwKCmBgYHtyfQoKcmZfZ3JpZCA8LSBleHBhbmQuZ3JpZChtdHJ5PWMoMSwyLDQsNiw4KSkKc2V0LnNlZWQoMTIzKQoKcmZfZGF0YSA8LSBjYmluZCh0cmFpbkRhdGFGZWF0dXJlcywgdHJhaW5EYXRhTGFiZWxzKQpyZl9tb2RlbCA8LSBjYXJldDo6dHJhaW4oc3Ryb2tlIH4uLGRhdGE9cmZfZGF0YSwgbWV0aG9kPSdyZicsIHRyQ29udHJvbD1teUNvbnRyb2wsIHByZVByb2MgPSAicmFuZ2UiLCBtZXRyaWM9J1JPQycsIHR1bmVHcmlkPXJmX2dyaWQpCgpyZl9wcmVkIDwtIHByZWRpY3QocmZfbW9kZWwsIHZhbERhdGFGZWF0dXJlcykKcmZfcHJvYiA8LSBwcmVkaWN0KHJmX21vZGVsLCB2YWxEYXRhRmVhdHVyZXMsIHR5cGU9J3Byb2InKQpgYGAKCmBgYCB7cn0KcmZfcmVzdWx0cyA8LSBjb25mdXNpb25NYXRyaXgocmZfcHJlZCx2YWxEYXRhTGFiZWxzJHN0cm9rZSwgcG9zaXRpdmU9IlllcyIpCnJmX3Jlc3VsdHMKcmZfcmVzdWx0czIgPC0gZGF0YS5mcmFtZShhcy5mYWN0b3IocmZfcHJlZCksIGFzLmZhY3Rvcih2YWxEYXRhTGFiZWxzJHN0cm9rZSksIHJmX3Byb2IpCmNvbG5hbWVzKHJmX3Jlc3VsdHMyKSA8LSBjKCJwcmVkIiwgIm9icyIsICJObyIsICJZZXMiKQoKCnJmXzJjbGFzcyA8LSB0d29DbGFzc1N1bW1hcnkocmZfcmVzdWx0czIsIGxldj1jKCJObyIsICJZZXMiKSkKcmZfMmNsYXNzWzFdCnJmX3JvYyA8LSByb2ModmFsRGF0YUxhYmVscyRzdHJva2UgfiByZl9wcm9iJFllcywgcGxvdD1UUlVFLCBwcmludC5hdWM9VFJVRSwgY29sPSdibGFjaycsIGx3ZD00LCBsZWdhY3kuYXhlcz1UUlVFLCBtYWluPSJST0MgQ3VydmUiKQp2YXJJbXAocmZfbW9kZWwsIHNjYWxlID0gVFJVRSkKcmZfbW9kZWwKcGxvdChyZl9tb2RlbCkKYGBgCgoKCgoKCiMjIExpbmVhciBTVk0gTW9kZWwKYGBge3IgZWNobz1UUlVFfQpzZXQuc2VlZCgxMjMpCmxzdm1fZ3JpZCA8LSBleHBhbmQuZ3JpZChDID0gMyoqKC03OjcpKQpsc3ZtX2RhdGEgPC0gY2JpbmQodHJhaW5EYXRhRmVhdHVyZXNfZSwgdHJhaW5EYXRhTGFiZWxzKQpsc3ZtX21vZGVsIDwtIGNhcmV0Ojp0cmFpbihzdHJva2Ugfi4sIGRhdGEgPSBsc3ZtX2RhdGEsIG1ldGhvZCA9ICJzdm1MaW5lYXIiLCB0ckNvbnRyb2wgPSBteUNvbnRyb2wsIHByZVByb2Nlc3MgPSAncmFuZ2UnLCB0dW5lR3JpZCA9IGxzdm1fZ3JpZCwgbWV0cmljPSAnUk9DJykKYGBgCgpgYGB7cn0KbHN2bV9wcmVkIDwtIHByZWRpY3QobHN2bV9tb2RlbCwgdmFsRGF0YUZlYXR1cmVzX2UpCmxzdm1fcHJvYiA8LSBwcmVkaWN0KGxzdm1fbW9kZWwsIHZhbERhdGFGZWF0dXJlc19lLCB0eXBlPSdwcm9iJykKbHN2bV9yZXN1bHRzIDwtIGNvbmZ1c2lvbk1hdHJpeChsc3ZtX3ByZWQsIHZhbERhdGFMYWJlbHMkc3Ryb2tlLCBwb3NpdGl2ZT0iWWVzIikKbHN2bV9yZXN1bHRzMiA8LSBkYXRhLmZyYW1lKGFzLmZhY3Rvcihsc3ZtX3ByZWQpLCBhcy5mYWN0b3IodmFsRGF0YUxhYmVscyRzdHJva2UpLCBsc3ZtX3Byb2IpCmxzdm1fcmVzdWx0cwoKY29sbmFtZXMobHN2bV9yZXN1bHRzMikgPC0gYygicHJlZCIsICJvYnMiLCAiTm8iLCAiWWVzIikKbHN2bV8yY2xhc3MgPC0gdHdvQ2xhc3NTdW1tYXJ5KGxzdm1fcmVzdWx0czIsIGxldj1jKCJObyIsICJZZXMiKSkKbHN2bV8yY2xhc3NbMV0KdmFySW1wKGxzdm1fbW9kZWwsIHNjYWxlID0gVFJVRSkKbHN2bV9yb2MgPC0gcm9jKHZhbERhdGFMYWJlbHMkc3Ryb2tlIH4gbHN2bV9wcm9iJFllcywgcGxvdD1UUlVFLCBwcmludC5hdWM9VFJVRSwgY29sPSdibGFjaycsIGx3ZD00LCBsZWdhY3kuYXhlcz1UUlVFLCBtYWluPSJST0MgQ3VydmUiLCBhZGQ9RkFMU0UpCgpwbG90KGxzdm1fbW9kZWwsIHNjYWxlcz1saXN0KHg9bGlzdChsb2c9MykpKQpsc3ZtX21vZGVsCgpgYGAKCiMgR3JhZGllbnQgQm9vc3QgTW9kZWwKCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0UsIGluY2x1ZGU9VFJVRX0KZ2JtX0dyaWQgPC0gIGV4cGFuZC5ncmlkKGludGVyYWN0aW9uLmRlcHRoID0gYygxLCAzLCA2LCA5LCAxMCksCiAgICAgICAgICAgICAgICAgICAgbi50cmVlcyA9IHNlcSgyNTAsMjAwMCwyNTApLCAKICAgICAgICAgICAgICAgICAgICBzaHJpbmthZ2UgPSBzZXEoLjAwMDUsIC4wNSwuMDAwNSksCiAgICAgICAgICAgICAgICAgICAgbi5taW5vYnNpbm5vZGUgPSAxMCkKCnNldC5zZWVkKDEyMykKZ2JtX2RhdGEgPSBjYmluZCh0cmFpbkRhdGFGZWF0dXJlc19lLCB0cmFpbkRhdGFMYWJlbHMpCmdibV9tb2RlbCA8LSBjYXJldDo6dHJhaW4oc3Ryb2tlIH4uLCBkYXRhID0gZ2JtX2RhdGEsIG1ldGhvZCA9ICJnYm0iLCB0ckNvbnRyb2wgPSBteUNvbnRyb2wsIHByZVByb2Nlc3MgPSAncmFuZ2UnLCBtZXRyaWM9ICdST0MnLCB0dW5lTGVuZ3RoID0gMTApCmBgYAoKYGBge3J9CmdibV9wcmVkIDwtIHByZWRpY3QoZ2JtX21vZGVsLCB2YWxEYXRhRmVhdHVyZXNfZSkKZ2JtX3Byb2IgPC0gcHJlZGljdChnYm1fbW9kZWwsIHZhbERhdGFGZWF0dXJlc19lLCB0eXBlPSdwcm9iJykKCmdibV9yZXN1bHRzIDwtIGNvbmZ1c2lvbk1hdHJpeChnYm1fcHJlZCwgdmFsRGF0YUxhYmVscyRzdHJva2UsIHBvc2l0aXZlPSdZZXMnKQoKZ2JtX3Jlc3VsdHMKCmdibV9yZXN1bHRzMiA8LSBkYXRhLmZyYW1lKGFzLmZhY3RvcihnYm1fcHJlZCksIGFzLmZhY3Rvcih2YWxEYXRhTGFiZWxzJHN0cm9rZSksIGdibV9wcm9iKQoKCmNvbG5hbWVzKGdibV9yZXN1bHRzMikgPC0gYygicHJlZCIsICJvYnMiLCAiTm8iLCAiWWVzIikKCgoKZ2JtXzJjbGFzcyA8LSB0d29DbGFzc1N1bW1hcnkoZ2JtX3Jlc3VsdHMyLCBsZXY9YygiTm8iLCAiWWVzIikpCmdibV8yY2xhc3NbMV0KZ2JtX3JvYyA8LSByb2ModmFsRGF0YUxhYmVscyRzdHJva2UgfiBnYm1fcHJvYiRZZXMsIHBsb3Q9VFJVRSwgcHJpbnQuYXVjPVRSVUUsIGNvbD0nYmxhY2snLCBsd2Q9NCwgbGVnYWN5LmF4ZXM9VFJVRSwgbWFpbj0iUk9DIEN1cnZlIiwgYWRkPUZBTFNFKQoKZ2JtX21vZGVsCnBsb3QoZ2JtX21vZGVsKQoKYGBgCgoKCiMgTmV1cmFsIE5ldHdvcmsgTW9kZWwKIyMgU2NhbGUgZGF0YXNldHMgZm9yIHVzYWdlCmBgYHtyfQoKbnVtZXJpY2FsIDwtIGMoMSw2LDcpCgoKdHJhaW5EYXRhRnVsbE51bSA8LSB0cmFpbkRhdGFGdWxsW251bWVyaWNhbF0KdHJhaW5EYXRhRnVsbENhdCA8LSB0cmFpbkRhdGFGdWxsWy1udW1lcmljYWxdCgoKCnRyYWluRGF0YU51bSA8LSB0cmFpbkRhdGFbbnVtZXJpY2FsXQp0cmFpbkRhdGFDYXQgPC0gdHJhaW5EYXRhWy1udW1lcmljYWxdCgoKdmFsRGF0YU51bSA8LSB2YWxEYXRhW251bWVyaWNhbF0KdmFsRGF0YUNhdCA8LSB2YWxEYXRhWy1udW1lcmljYWxdCgoKdGVzdERhdGFOdW0gPC0gdGVzdERhdGFbbnVtZXJpY2FsXQp0ZXN0RGF0YUNhdCA8LSB0ZXN0RGF0YVstbnVtZXJpY2FsXQoKdHJhaW5EYXRhRnVsbFNjYWxlZCA8LSBzY2FsZSh0cmFpbkRhdGFGdWxsTnVtKQpjb2xfbWVhbnNfdHJhaW4gPC0gYXR0cih0cmFpbkRhdGFGdWxsU2NhbGVkLCAic2NhbGVkOmNlbnRlciIpCmNvbF9zdGRkZXZzX3RyYWluIDwtIGF0dHIodHJhaW5EYXRhRnVsbFNjYWxlZCwgInNjYWxlZDpzY2FsZSIpCgp0ZXN0RGF0YVNjYWxlZCA8LSBzY2FsZSh0ZXN0RGF0YU51bSwgY2VudGVyID0gY29sX21lYW5zX3RyYWluLCBzY2FsZSA9IGNvbF9zdGRkZXZzX3RyYWluKSAgCgp0cmFpbkRhdGFTY2FsZWQgPC0gc2NhbGUodHJhaW5EYXRhTnVtKQpjb2xfbWVhbnNfdHJhaW4gPC0gYXR0cih0cmFpbkRhdGFTY2FsZWQsICJzY2FsZWQ6Y2VudGVyIikKY29sX3N0ZGRldnNfdHJhaW4gPC0gYXR0cih0cmFpbkRhdGFTY2FsZWQsICJzY2FsZWQ6c2NhbGUiKQoKdmFsRGF0YVNjYWxlZCA8LSBzY2FsZSh2YWxEYXRhTnVtLCBjZW50ZXIgPSBjb2xfbWVhbnNfdHJhaW4sIHNjYWxlID0gY29sX3N0ZGRldnNfdHJhaW4pICAKCm5uRnVsbFRyYWluRGF0YSA8LSBjYmluZCh0cmFpbkRhdGFGdWxsU2NhbGVkLCB0cmFpbkRhdGFGdWxsQ2F0KQpublRyYWluRGF0YSA8LSBjYmluZCh0cmFpbkRhdGFTY2FsZWQsIHRyYWluRGF0YUNhdCkKbm5UZXN0RGF0YSA8LSBjYmluZCh0ZXN0RGF0YVNjYWxlZCwgdGVzdERhdGFDYXQpCm5uVmFsRGF0YSA8LSBjYmluZCh2YWxEYXRhU2NhbGVkLCB2YWxEYXRhQ2F0KQoKCiMgYmFsYW5jZSB0cmFpbmluZyBkYXRhCgpublRyYWluRGF0YSA8LSBST1NFKHN0cm9rZSB+IC4sIGRhdGE9bm5UcmFpbkRhdGEsIHA9MC41KSRkYXRhCgpubkZ1bGxUcmFpbkRhdGEgPC0gUk9TRShzdHJva2UgfiAuLCBkYXRhPW5uRnVsbFRyYWluRGF0YSwgcD0wLjUpJGRhdGEKCm5uVmFsQmFsIDwtIFJPU0Uoc3Ryb2tlIH4gLiwgZGF0YT1ublZhbERhdGEsIHA9MC41KSRkYXRhCgpgYGAKCiMjIHNlcGFyYXRlIGZlYXR1cmVzIGZyb20gbGFiZWxzCgpgYGB7cn0Kbm5GdWxsVHJhaW5GZWF0dXJlcyA8LSBubkZ1bGxUcmFpbkRhdGFbLTldCm5uRnVsbFRyYWluTGFiZWxzIDwtIG5uRnVsbFRyYWluRGF0YVs5XQoKbm5UcmFpbkZlYXR1cmVzIDwtIG5uVHJhaW5EYXRhWy05XQpublRyYWluTGFiZWxzIDwtIG5uVHJhaW5EYXRhWzldCgpublRlc3RGZWF0dXJlcyA8LSBublRlc3REYXRhWy05XQpublRlc3RMYWJlbHMgPC0gbm5UZXN0RGF0YVs5XQoKbm5WYWxGZWF0dXJlcyA8LSBublZhbERhdGFbLTldCm5uVmFsTGFiZWxzIDwtIG5uVmFsRGF0YVs5XQoKbm5WYWxCYWxGZWF0dXJlcyA8LSBublZhbEJhbFstOV0Kbm5WYWxCYWxMYWJlbHMgPC0gbm5WYWxCYWxbOV0KCm5uVmFsTGFiZWxzJHN0cm9rZSA8LSBpZmVsc2Uobm5WYWxMYWJlbHMkc3Ryb2tlID09ICdObycsIDAsIDEpCm5uVGVzdExhYmVscyRzdHJva2UgPC0gaWZlbHNlKG5uVGVzdExhYmVscyRzdHJva2UgPT0gJ05vJywgMCwgMSkKbm5UcmFpbkxhYmVscyRzdHJva2UgPC0gaWZlbHNlKG5uVHJhaW5MYWJlbHMkc3Ryb2tlID09ICdObycsIDAsIDEpCm5uRnVsbFRyYWluTGFiZWxzJHN0cm9rZSA8LSBpZmVsc2Uobm5GdWxsVHJhaW5MYWJlbHMkc3Ryb2tlID09ICdObycsIDAsIDEpCm5uVmFsQmFsTGFiZWxzJHN0cm9rZSA8LSBpZmVsc2Uobm5WYWxCYWxMYWJlbHMkc3Ryb2tlID09ICdObycsIDAsIDEpCgoKYGBgCgpgYGB7ciBlY2hvPUZBTFNFfQpwYW5kb2MuaGVhZGVyKCJTdHJva2UgRGlzdHJ1YnV0aW9uIGZvciBUcmFpbiBEYXRhc2V0IiwzKQpwYW5kb2MudGFibGUodGFibGUobm5UcmFpbkxhYmVscyRzdHJva2UpKQoKcGFuZG9jLmhlYWRlcigiU3Ryb2tlIERpc3RydWJ1dGlvbiBmb3IgZnVsbFRyYWluIERhdGFzZXQiLDMpCnBhbmRvYy50YWJsZSh0YWJsZShubkZ1bGxUcmFpbkxhYmVscyRzdHJva2UpKQpgYGAKCgojIyBvbmUtaG90IGVuY29kZSBjYXRlZ29yaWNhbCBmZWF0dXJlcwoKYGBge3J9CgpkdW1teUZ1bGxUcmFpbiA8LSBkdW1teVZhcnMoIn4gLiIsIGRhdGE9bm5GdWxsVHJhaW5GZWF0dXJlcykgCmR1bW15VHJhaW4gPC0gZHVtbXlWYXJzKCJ+IC4iLCBkYXRhPW5uVHJhaW5GZWF0dXJlcykKCm5uRnVsbFRyYWluRmVhdHVyZXMgPC0gZGF0YS5mcmFtZShwcmVkaWN0KGR1bW15RnVsbFRyYWluLG5ld2RhdGE9bm5GdWxsVHJhaW5GZWF0dXJlcykpCm5uVGVzdEZlYXR1cmVzIDwtIGRhdGEuZnJhbWUocHJlZGljdChkdW1teUZ1bGxUcmFpbixuZXdkYXRhPW5uVGVzdEZlYXR1cmVzKSkKCm5uVHJhaW5GZWF0dXJlcyA8LSBkYXRhLmZyYW1lKHByZWRpY3QoZHVtbXlUcmFpbixuZXdkYXRhPW5uVHJhaW5GZWF0dXJlcykpCm5uVmFsRmVhdHVyZXMgPC0gZGF0YS5mcmFtZShwcmVkaWN0KGR1bW15VHJhaW4sbmV3ZGF0YT1ublZhbEZlYXR1cmVzKSkKbm5WYWxCYWxGZWF0dXJlcyA8LSBkYXRhLmZyYW1lKHByZWRpY3QoZHVtbXlUcmFpbixuZXdkYXRhPW5uVmFsQmFsRmVhdHVyZXMpKQoKCmBgYAoKCgoKCgojIyBOZXVyYWwgTmV0d29yayBNb2RlbCBUdW5pbmcKYGBge3IgZWNobz1UUlVFLCBtZXNzYWdlPUZBTFNFLCByZXN1bHRzPSdoaWRlJ30KIApjbGVhbl9ydW5zKGNvbmZpcm09RkFMU0UpCnNldC5zZWVkKDEyMykKcnVucyA8LSB0dW5pbmdfcnVuKCJhbm4uUiIsIGZsYWdzID0gbGlzdChub2RlczEgPSBjKDIwLCA1MCw3NSksIG5vZGVzMj1jKDIwLDUwLDc1KSwgZHJvcDE9YygwLjIsMC40LDAuNiksIGRyb3AyPWMoMC4yLDAuNCwwLjYpLCBsZWFybmluZ19yYXRlID0gYygwLjAxLDAuMDUsMC4wMDEsMC4wMDAxKSwgZXBvY2hzID0gYygyNSw1MCwxMDApLCBiYXRjaF9zaXplID0gYygxMCwyNSw1MCksIGFjdGl2YXRpb24gPSBjKCJyZWx1IiwgInNpZ21vaWQiLCAidGFuaCIpKSxzYW1wbGUgPSAwLjAyLCBjb25maXJtPUZBTFNFLCBlY2hvID0gRkFMU0UpCmBgYAoKYGBge3J9CnJ1bmRmIDwtIGxzX3J1bnMob3JkZXI9J21ldHJpY192YWxfYWNjdXJhY3knLCBkZWNyZWFzaW5nPVRSVUUpCnJ1bmRmIAoKYmVzdHJ1biA8LSBydW5kZlsxLDE6MTldCmBgYAoKYGBge3IgZWNobz1GQUxTRSwgcmVzdWx0cz0nYXNpcyd9CnBhbmRvYy5oZWFkZXIoIkJlc3QgUnVuIERldGFpbHMiLDMpCnBhbmRvYy50YWJsZSh0KGJlc3RydW4pKQpgYGAKCiMjIENyZWF0ZSBtb2RlbCB1c2luZyBwYXJhbXRlcnMgZnJvbSBiZXN0IHR1bmluZyBydW4KCgoKYGBge3IgaW5jbHVkZT1UUlVFLCByZXN1bHRzPSdoaWRlJ30KbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpCm1vZGVsICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gNTAsIGFjdGl2YXRpb24gPSAicmVsdSIsIGlucHV0X3NoYXBlID0gZGltKG5uVHJhaW5GZWF0dXJlcylbMl0pICU+JQogIGxheWVyX2Ryb3BvdXQocmF0ZT0wLjQpICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gNTAsIGFjdGl2YXRpb24gPSAicmVsdSIpICU+JQogIGxheWVyX2Ryb3BvdXQocmF0ZT0wLjYpICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gMSwgYWN0aXZhdGlvbiA9ICdzaWdtb2lkJykKCm1vZGVsICU+JSBjb21waWxlKAogIG9wdGltaXplciA9IG9wdGltaXplcl9hZGFtKGxyPTAuMDUpLAogIAogIGxvc3M9J2JpbmFyeV9jcm9zc2VudHJvcHknLAogIG1ldHJpY3MgPSBjKCdhY2N1cmFjeScpCiAKKQoKc2V0LnNlZWQoMjM0KQpoaXN0b3J5IDwtIG1vZGVsICU+JSBmaXQoYXMubWF0cml4KG5uVHJhaW5GZWF0dXJlcyksIGFzLm1hdHJpeChublRyYWluTGFiZWxzKSwgZXBvY2hzPTUwLCBiYXRjaF9zaXplPTEwLHZhbGlkYXRpb25fZGF0YT1saXN0KGFzLm1hdHJpeChublZhbEZlYXR1cmVzKSwgYXMubWF0cml4KG5uVmFsTGFiZWxzKSkpCmBgYAojIyMgRXZhbHVhdGUgTk4gTW9kZWwgUGVyZm9ybWFuY2UKYGBge3J9CnNldC5zZWVkKDEyMykKbW9kZWwgJT4lIGV2YWx1YXRlKGFzLm1hdHJpeChublZhbEZlYXR1cmVzKSwgYXMubWF0cml4KG5uVmFsTGFiZWxzKSkKCm5uX3ByZWQgPC0gbW9kZWwgJT4lIHByZWRpY3RfY2xhc3Nlcyhhcy5tYXRyaXgobm5WYWxGZWF0dXJlcykpIApubl9wcm9iIDwtIG1vZGVsICU+JSBwcmVkaWN0X3Byb2JhKGFzLm1hdHJpeChublZhbEZlYXR1cmVzKSkKbm5fcHJvYiA8LSBkYXRhLmZyYW1lKG5uX3Byb2IpCmNvbG5hbWVzKG5uX3Byb2IpIDwtIGMoJ1llcycpCm5uX3Byb2IkTm8gPC0gMSAtIG5uX3Byb2IkWWVzCm5uX3Jlc3VsdHMyIDwtIGRhdGEuZnJhbWUobm5fcHJlZCwgbm5WYWxMYWJlbHMsIG5uX3Byb2IpIAoKY29sbmFtZXMobm5fcmVzdWx0czIpIDwtIGMoInByZWQiLCAib2JzIiwgIlllcyIsICJObyIpCm5uX3Jlc3VsdHMyJHByZWQgPC1hcy5mYWN0b3IoaWZlbHNlKG5uX3Jlc3VsdHMyJHByZWQgPT0gMCwgJ05vJywgJ1llcycpKQpubl9yZXN1bHRzMiRvYnMgPC1hcy5mYWN0b3IoaWZlbHNlKG5uX3Jlc3VsdHMyJG9icyA9PSAwLCAnTm8nLCAnWWVzJykpCm5uXzJjbGFzcyA8LSB0d29DbGFzc1N1bW1hcnkobm5fcmVzdWx0czIsIGxldj1jKCJObyIsICJZZXMiKSkKY29uZnVzaW9uTWF0cml4KG5uX3Jlc3VsdHMyJHByZWQsbm5fcmVzdWx0czIkb2JzLCBwb3NpdGl2ZT0nWWVzJykKbm5fMmNsYXNzWzFdCnBsb3QoaGlzdG9yeSkKbm5fcm9jIDwtIHJvYyhublZhbExhYmVscyRzdHJva2UgfiBubl9wcm9iJFllcywgcGxvdD1UUlVFLCBwcmludC5hdWM9VFJVRSwgY29sPSdibGFjaycsIGx3ZD00LCBsZWdhY3kuYXhlcz1UUlVFLCBtYWluPSJST0MgQ3VydmUiLCBhZGQ9RkFMU0UpCgpgYGAKCgoKIyBNb2RlbCBDb21wYXJpc29uCmBgYHtyIGVjaG89RkFMU0UsIHJlc3VsdHM9J2FzaXMnfQpjb21wYXJlZCA8LSBkYXRhLmZyYW1lKGtubl8yY2xhc3MsIGdsbV8yY2xhc3MsIHRyZWVfMmNsYXNzLCByZl8yY2xhc3MsIGxzdm1fMmNsYXNzLCBnYm1fMmNsYXNzLCBubl8yY2xhc3MpCmNvbG5hbWVzKGNvbXBhcmVkKSA8LSBjKCJrbm4iLCAiZ2xtbmV0IiwgInJwYXJ0IiwgInJmIiwgInN2bUxpbmVhciIsICJnYm0iLCAibmV1cmFsIG5ldHdvcmsiKQpjb21wYXJlZCA8LSB0KGNvbXBhcmVkKQpjb2xuYW1lcyhjb21wYXJlZCkgPC0gYygiUk9DIiwgIlNwZWMiLCAiU2VucyIpCnBhbmRvYy5oZWFkZXIoIk1vZGVsIFBlcmZvcm1hY2UgQ29tcGFyaXNvbiIsMykKcGFuZG9jLnRhYmxlKGNvbXBhcmVkKQpgYGAgCgpgYGB7ciByZXN1bHRzPSdoaWRlJ30gCnByZWRfcmY9IHByZWRpY3Rpb24ocmZfcHJvYiRZZXMsdmFsRGF0YUxhYmVscyRzdHJva2UpCgpwZXJmb3JtYW5jZShwcmVkX3JmLCBtZWFzdXJlID0gImF1YyIpQHkudmFsdWVzCnJmX3JvYyA8LSByb2ModmFsRGF0YUxhYmVscyRzdHJva2UgfiByZl9wcm9iJFllcywgcGxvdD1UUlVFLCBwcmludC5hdWM9RkFMU0UsIGNvbD0nZ3JlZW4nLCBsd2Q9NCwgbGVnYWN5LmF4ZXM9VFJVRSwgbWFpbj0iUk9DIEN1cnZlIikKCnByZWRfa25uID0gcHJlZGljdGlvbihrbm5fcHJvYiRZZXMsIHZhbERhdGFMYWJlbHMkc3Ryb2tlKQpwZXJmb3JtYW5jZShwcmVkX2tubiwgbWVhc3VyZT0nYXVjJylAeS52YWx1ZXMKa25uX3JvYyA8LSByb2ModmFsRGF0YUxhYmVscyRzdHJva2UgfiBrbm5fcHJvYiRZZXMsIHBsb3Q9VFJVRSwgcHJpbnQuYXVjPUZBTFNFLCBjb2w9J2JsdWUnLCBsd2Q9NCwgbGVnYWN5LmF4ZXM9VFJVRSwgbWFpbj0iUk9DIEN1cnZlIiwgYWRkPVRSVUUpCgpwcmVkX2dsbSA8LSBwcmVkaWN0aW9uKGdsbV9wcm9iJFllcywgdmFsRGF0YUxhYmVscyRzdHJva2UpCmdsbV9wZXJmb3JtIDwtIHBlcmZvcm1hbmNlKHByZWRfZ2xtLCBtZWFzdXJlPSdhdWMnKUB5LnZhbHVlcwpnbG1fcm9jIDwtIHJvYyh2YWxEYXRhTGFiZWxzJHN0cm9rZSB+IGdsbV9wcm9iJFllcywgcGxvdD1UUlVFLCBwcmludC5hdWM9RkFMU0UsIGNvbD0nb3JhbmdlJywgbHdkPTQsIGxlZ2FjeS5heGVzPVRSVUUsIG1haW49IlJPQyBDdXJ2ZSIsIGFkZD1UUlVFKQoKcHJlZF90cmVlIDwtIHByZWRpY3Rpb24odHJlZV9wcm9iJFllcywgdmFsRGF0YUxhYmVscyRzdHJva2UpCnBlcmZvcm1hbmNlKHByZWRfdHJlZSwgbWVhc3VyZT0nYXVjJylAeS52YWx1ZXMKdHJlZV9yb2MgPC0gcm9jKHZhbERhdGFMYWJlbHMkc3Ryb2tlIH4gdHJlZV9wcm9iJFllcywgcGxvdD1UUlVFLCBwcmludC5hdWM9RkFMU0UsIGNvbD0neWVsbG93JywgbHdkPTQsIGxlZ2FjeS5heGVzPVRSVUUsIG1haW49IlJPQyBDdXJ2ZSIsIGFkZD1UUlVFKQoKCnByZWRfbHN2bSA8LSBwcmVkaWN0aW9uKGxzdm1fcHJvYiRZZXMsIHZhbERhdGFMYWJlbHMkc3Ryb2tlKQpwZXJmb3JtYW5jZShwcmVkX2xzdm0sIG1lYXN1cmU9J2F1YycpQHkudmFsdWVzCmxzdm1fcm9jIDwtIHJvYyh2YWxEYXRhTGFiZWxzJHN0cm9rZSB+IGxzdm1fcHJvYiRZZXMsIHBsb3Q9VFJVRSwgcHJpbnQuYXVjPUZBTFNFLCBjb2w9J3B1cnBsZScsIGx3ZD00LCBsZWdhY3kuYXhlcz1UUlVFLCBtYWluPSJST0MgQ3VydmUiLCBhZGQ9VFJVRSkKCnByZWRfZ2JtIDwtIHByZWRpY3Rpb24oZ2JtX3Byb2IkWWVzLCB2YWxEYXRhTGFiZWxzJHN0cm9rZSkKcGVyZm9ybWFuY2UocHJlZF9nYm0sIG1lYXN1cmU9J2F1YycpQHkudmFsdWVzCmdibV9yb2MgPC0gcm9jKHZhbERhdGFMYWJlbHMkc3Ryb2tlIH4gZ2JtX3Byb2IkWWVzLCBwbG90PVRSVUUsIHByaW50LmF1Yz1GQUxTRSwgY29sPSdibGFjaycsIGx3ZD00LCBsZWdhY3kuYXhlcz1UUlVFLCBtYWluPSJST0MgQ3VydmUiLCBhZGQ9VFJVRSkKCnByZWRfbm4gPSBwcmVkaWN0aW9uKG5uX3Byb2IkWWVzLG5uVmFsTGFiZWxzJHN0cm9rZSkKcGVyZm9ybWFuY2UocHJlZF9ubiwgbWVhc3VyZT0nYXVjJylAeS52YWx1ZXMKbm5fcm9jIDwtIHJvYyhublZhbExhYmVscyRzdHJva2UgfiBubl9wcm9iJFllcywgcGxvdD1UUlVFLCBwcmludC5hdWM9RkFMU0UsIGNvbD0nYnJvd24nLCBsd2Q9NCwgbGVnYWN5LmF4ZXM9VFJVRSwgbWFpbj0iUk9DIEN1cnZlIiwgYWRkPVRSVUUpCgpsZWdlbmQoImJvdHRvbXJpZ2h0IiwgbGVnZW5kPWMoImtubiIsICdyZicsICdnbG1uZXQnLCAncnBhcnQnLCAnc3ZtTGluZWFyJywgJ2dibScsICduZXVyYWwgbmV0d29yaycpLCBjb2w9YygiYmx1ZSIsICJncmVlbiIsICdvcmFuZ2UnLCAneWVsbG93JywgJ3B1cnBsZScsICdibGFjaycsICdicm93bicpLCBsd2Q9MikKCmBgYAoKCgoKIyBCdWlsZCBGaW5hbCBNb2RlbCAgIAojIyMgQmFzZWQgdXBvbiBUcmFpbi9WYWxpZGF0aW9uIHJ1bnMgb2YgdGhlIG1vZGVscywgdGhlIEVsYXN0aWMgTmV0IG1vZGVsIGhhdmluZyBST0M6IDAuODQ5MywgU3BlY2lmaWNpdHk6IDAuNzA3OSBhbmQgU2Vuc2l0aXZpdHk6IDAuODUwMCBzaG93ZWQgdGhlIGJlc3QgcGVyZm9ybWFuY2UuIFdlIHdpbGwgbm93IHRyYWluIGEgZ2xtbmV0IG1vZGVsIHVzaW5nIHRoZSBmdWxsIHRyYWluaW5nIGRhdGEgc2V0IGFuZCBldmFsdWF0ZSBpdCB1c2luZyB0aGUgdGVzdCBkYXRhc2V0LgpgYGB7cn0KZnVsbERhdGEgPC0gY2JpbmQodHJhaW5EYXRhRnVsbEJhbGFuY2VkRmVhdHVyZXNfc2UsIHRyYWluRGF0YUZ1bGxCYWxhbmNlZExhYmVscykKCmZpbmFsX21vZGVsIDwtIGdsbW5ldCh4PWFzLm1hdHJpeCh0cmFpbkRhdGFGdWxsQmFsYW5jZWRGZWF0dXJlc19zZSksIHk9YXMubWF0cml4KHRyYWluRGF0YUZ1bGxCYWxhbmNlZExhYmVscyksIGZhbWlseT0nYmlub21pYWwnLCBhbHBoYT1nbG1fbW9kZWwkYmVzdFR1bmUkYWxwaGEsIGxhbWJkYT1nbG1fbW9kZWwkYmVzdFR1bmUkbGFtYmRhKQpmaW5hbF9wcmVkaWN0aW9uIDwtIHByZWRpY3QoZmluYWxfbW9kZWwsIGFzLm1hdHJpeCh0ZXN0RGF0YUZlYXR1cmVzX3NlKSx0eXBlPSdjbGFzcycpCmZpbmFsX3Byb2JzIDwtIHByZWRpY3QoZmluYWxfbW9kZWwsIGFzLm1hdHJpeCh0ZXN0RGF0YUZlYXR1cmVzX3NlKSx0eXBlPSdyZXNwb25zZScpCmZpbmFsX3ByZWRpY3Rpb24gPC0gYXMuZmFjdG9yKGZpbmFsX3ByZWRpY3Rpb24pCmZpbmFsX3Jlc3VsdHMgPC0gZGF0YS5mcmFtZShmaW5hbF9wcmVkaWN0aW9uLCB0ZXN0RGF0YUxhYmVscywgZmluYWxfcHJvYnMpCmNvbG5hbWVzKGZpbmFsX3Jlc3VsdHMpIDwtIGMoInByZWQiLCAib2JzIiwgIlllcyIpCmZpbmFsX3Jlc3VsdHMkTm8gPC0gMS0gZmluYWxfcmVzdWx0cyRZZXMKCnByZWRfZmluYWw9IHByZWRpY3Rpb24oZmluYWxfcmVzdWx0cyRZZXMsdGVzdERhdGFMYWJlbHMkc3Ryb2tlKQoKcGVyZm9ybWFuY2UocHJlZF9maW5hbCwgbWVhc3VyZSA9ICJhdWMiKUB5LnZhbHVlcwpmaW5hbF9yb2MgPC0gcm9jKHRlc3REYXRhTGFiZWxzJHN0cm9rZSB+IGZpbmFsX3Jlc3VsdHMkWWVzLCBwbG90PVRSVUUsIAogICAgICAgICAgICAgICAgIHByaW50LmF1Yz1UUlVFLCBjb2w9J3JlZCcsIGx3ZD00LCBsZWdhY3kuYXhlcz1UUlVFLCBtYWluPSJST0MgQ3VydmUiKQpjb25mdXNpb25NYXRyaXgoZmluYWxfcHJlZGljdGlvbiwgdGVzdERhdGFMYWJlbHMkc3Ryb2tlLCBwb3NpdGl2ZT0iWWVzIikKY29lZihmaW5hbF9tb2RlbCkKCmBgYAoKIyMjIEZpbmFsIG1ldHJpY3MgZm9yIHRoZSBnbG1uZXQgbW9kZWwgYXJlIGNvbnNpc3RlbnQgd2l0aCB3aGF0IHdhcyBmb3VuZCBkdXJpbmcgdGhlIGNyb3NzLXZhbGlkYXRpb24gYW5kIHR1bmluZyBwcm9jZXNzCg==